1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 09:19:40 +01:00
psalm/tests/StubTest.php

1476 lines
42 KiB
PHP
Raw Normal View History

<?php
namespace Psalm\Tests;
2021-06-08 04:55:21 +02:00
use Psalm\Config;
use Psalm\Context;
2021-12-03 20:29:06 +01:00
use Psalm\Exception\CodeException;
use Psalm\Exception\ConfigException;
use Psalm\Exception\InvalidClasslikeOverrideException;
use Psalm\Exception\InvalidMethodOverrideException;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Analyzer\ProjectAnalyzer;
2021-06-08 04:55:21 +02:00
use Psalm\Internal\IncludeCollector;
use Psalm\Internal\Provider\FakeFileProvider;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Provider\Providers;
2021-06-08 04:55:21 +02:00
use Psalm\Internal\RuntimeCaches;
2021-12-04 21:55:53 +01:00
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
2021-06-08 04:55:21 +02:00
2019-07-05 22:24:00 +02:00
use function define;
use function defined;
use function dirname;
2021-06-08 04:55:21 +02:00
use function explode;
2019-07-05 22:24:00 +02:00
use function getcwd;
use function implode;
2021-12-03 21:07:25 +01:00
use function reset;
use function strlen;
use function strpos;
use function substr;
2021-06-08 04:55:21 +02:00
use const DIRECTORY_SEPARATOR;
class StubTest extends TestCase
{
2022-12-16 19:58:47 +01:00
protected static TestConfig $config;
public static function setUpBeforeClass(): void
{
self::$config = new TestConfig();
if (!defined('PSALM_VERSION')) {
define('PSALM_VERSION', '4.0.0');
}
if (!defined('PHP_PARSER_VERSION')) {
define('PHP_PARSER_VERSION', '4.0.0');
}
}
public function setUp(): void
{
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
RuntimeCaches::clearAll();
$this->file_provider = new FakeFileProvider();
}
2021-12-03 20:11:20 +01:00
private function getProjectAnalyzerWithConfig(Config $config): ProjectAnalyzer
{
2021-12-03 20:11:20 +01:00
$project_analyzer = new ProjectAnalyzer(
$config,
2021-12-03 20:11:20 +01:00
new Providers(
$this->file_provider,
2022-12-18 17:15:15 +01:00
new FakeParserCacheProvider(),
),
);
$project_analyzer->setPhpVersion('7.4', 'tests');
$config->setIncludeCollector(new IncludeCollector());
$config->visitComposerAutoloadFiles($project_analyzer, null);
2018-11-11 18:01:14 +01:00
return $project_analyzer;
}
public function testNonexistentStubFile(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(ConfigException::class);
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('Cannot resolve stubfile path');
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="stubs/invalidfile.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
}
public function testStubFileClass(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/systemclass.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace A\B\C;
$a = new \SystemClass();
$b = $a->foo(5, "hello");
$c = \SystemClass::bar(5, "hello");
2022-12-18 17:15:15 +01:00
echo \SystemClass::HELLO;',
);
$this->analyzeFile($file_path, new Context());
}
/**
* @psalm-pure
*/
private function getOperatingSystemStyledPath(string $file): string
{
return implode(DIRECTORY_SEPARATOR, explode('/', $file));
}
public function testLoadStubFileWithRelativePath(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<stubs>
<file name="./tests/../tests/fixtures/stubs/systemclass.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$path = $this->getOperatingSystemStyledPath('tests/fixtures/stubs/systemclass.phpstub');
2020-06-21 17:43:08 +02:00
$stub_files = $this->project_analyzer->getConfig()->getStubFiles();
2021-12-03 21:07:25 +01:00
$this->assertStringContainsString($path, reset($stub_files));
}
public function testLoadStubFileWithAbsolutePath(): void
{
$runDir = dirname(__DIR__);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
$runDir,
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<stubs>
<file name="' . $runDir . '/tests/fixtures/stubs/systemclass.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$path = $this->getOperatingSystemStyledPath('tests/fixtures/stubs/systemclass.phpstub');
2020-06-21 17:43:08 +02:00
$stub_files = $this->project_analyzer->getConfig()->getStubFiles();
2021-12-03 21:07:25 +01:00
$this->assertStringContainsString($path, reset($stub_files));
}
public function testStubFileConstant(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/systemclass.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace A\B\C;
$d = ROOT_CONST_CONSTANT;
$e = \ROOT_CONST_CONSTANT;
$f = ROOT_DEFINE_CONSTANT;
2022-12-18 17:15:15 +01:00
$g = \ROOT_DEFINE_CONSTANT;',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileParentClass(): void
{
$this->expectException(CodeException::class);
$this->expectExceptionMessage('ImplementedParamTypeMismatch');
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/systemclass.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace A\B\C;
class Foo extends \SystemClass
{
public function foo(string $a, string $b): string
{
return $a . $b;
}
}
2022-12-18 17:15:15 +01:00
',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileCircularReference(): void
{
$this->expectException(CodeException::class);
$this->expectExceptionMessage('CircularReference');
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/CircularReference.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class Foo extends Baz {}
2022-12-18 17:15:15 +01:00
',
);
$this->analyzeFile($file_path, new Context());
}
public function testPhpStormMetaParsingFile(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/phpstorm.meta.php" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Ns {
class MyClass {
public const OBJECT = "object";
2023-01-24 12:31:58 +01:00
private const EXCEPTION = "exception";
/**
* @return mixed
* @psalm-suppress InvalidReturnType
*/
public function create(string $s) {}
/**
* @return mixed
* @psalm-suppress InvalidReturnType
*/
public function create2(string $s) {}
/**
* @return mixed
* @psalm-suppress InvalidReturnType
*/
public function create3(string $s) {}
/**
* @param mixed $s
* @return mixed
* @psalm-suppress InvalidReturnType
*/
public function foo($s) {}
/**
* @return mixed
* @psalm-suppress InvalidReturnType
*/
public function bar(array $a) {}
}
}
namespace {
/**
* @return mixed
* @psalm-suppress InvalidReturnType
*/
function create(string $s) {}
/**
* @return mixed
* @psalm-suppress InvalidReturnType
*/
function create2(string $s) {}
/**
* @param mixed $s
* @return mixed
* @psalm-suppress InvalidReturnType
*/
function foo($s) {}
/**
* @return mixed
* @psalm-suppress InvalidReturnType
*/
function bar(array $a) {}
$a1 = (new \Ns\MyClass)->creAte("object");
$a2 = (new \Ns\MyClass)->creaTe("exception");
$y1 = (new \Ns\MyClass)->creAte2("object");
$y2 = (new \Ns\MyClass)->creaTe2("exception");
$const1 = (new \Ns\MyClass)->creAte3(\Ns\MyClass::OBJECT);
$const2 = (new \Ns\MyClass)->creaTe3("exception");
$b1 = \Create("object");
$b2 = \cReate("exception");
$e2 = \creAte(\LogicException::class);
$z1 = \Create2("object");
$z2 = \cReate2("exception");
$x2 = \creAte2(\LogicException::class);
$c1 = (new \Ns\MyClass)->foo(5);
$c2 = (new \Ns\MyClass)->bar(["hello"]);
$d1 = \foO(5);
$d2 = \baR(["hello"]);
2022-12-18 17:15:15 +01:00
}',
);
$context = new Context();
$this->analyzeFile($file_path, $context);
$this->assertContextVars(
[
'$a1===' => 'stdClass',
'$a2===' => 'Exception',
'$y1===' => 'stdClass',
'$y2===' => 'Exception',
'$const1===' => 'stdClass',
'$const2===' => 'Exception',
'$b1===' => 'stdClass',
'$b2===' => 'Exception',
'$e2===' => 'LogicException',
'$z1===' => 'stdClass',
'$z2===' => 'Exception',
'$x2===' => 'LogicException',
'$c1===' => "5",
'$c2===' => "'hello'",
'$d1===' => "5",
2022-12-21 00:18:50 +01:00
'$d2===' => "'hello'",
],
2022-12-21 00:18:50 +01:00
$context,
);
}
/** @param array<string, string> $assertions */
private function assertContextVars(array $assertions, Context $context): void
{
$actual_vars = [];
foreach ($assertions as $var => $_) {
$exact = false;
if ($var && strpos($var, '===') === strlen($var) - 3) {
$var = substr($var, 0, -3);
$exact = true;
}
if (isset($context->vars_in_scope[$var])) {
$value = $context->vars_in_scope[$var]->getId($exact);
if ($exact) {
$actual_vars[$var . '==='] = $value;
} else {
$actual_vars[$var] = $value;
}
}
}
$this->assertSame($assertions, $actual_vars);
}
public function testNamespacedStubClass(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/namespaced_class.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
$a = new Foo\SystemClass();
echo Foo\SystemClass::HELLO;
$b = $a->foo(5, "hello");
$c = Foo\SystemClass::bar(5, "hello");
2022-12-18 17:15:15 +01:00
echo Foo\BAR;',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubRegularFunction(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/custom_functions.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2022-12-18 17:15:15 +01:00
echo barBar("hello");',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubVariadicFunction(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/custom_functions.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2022-12-18 17:15:15 +01:00
variadic("bat", "bam");',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubVariadicFunctionWrongArgType(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('InvalidScalarArgument');
2021-12-03 20:29:06 +01:00
$this->expectException(CodeException::class);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/custom_functions.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2022-12-18 17:15:15 +01:00
variadic("bat", 5);',
);
$this->analyzeFile($file_path, new Context());
}
public function testUserVariadicWithFalseVariadic(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('TooManyArguments');
2021-12-03 20:29:06 +01:00
$this->expectException(CodeException::class);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
/**
* @param string ...$bar
*/
function variadic() : void {}
2022-12-18 17:15:15 +01:00
variadic("hello");',
);
$this->analyzeFile($file_path, new Context());
}
/**
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
* @runInSeparateProcess
*/
public function testPolyfilledFunction(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
autoloader="tests/fixtures/stubs/polyfill.phpstub"
>
<projectFiles>
<directory name="src" />
</projectFiles>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
$a = random_bytes(16);
2022-12-18 17:15:15 +01:00
$b = new_random_bytes(16);',
);
$this->analyzeFile($file_path, new Context());
}
/**
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
* @runInSeparateProcess
*/
public function testConditionalConstantDefined(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
autoloader="tests/fixtures/stubs/conditional_constant_define_inferred.phpstub"
>
<projectFiles>
<directory name="src" />
</projectFiles>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
/** @psalm-suppress MixedArgument */
2022-12-18 17:15:15 +01:00
echo CODE_DIR;',
);
$this->analyzeFile($file_path, new Context());
}
2018-11-29 06:05:56 +01:00
/**
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
* @runInSeparateProcess
2018-11-29 06:05:56 +01:00
*/
public function testClassAlias(): void
2018-11-29 06:05:56 +01:00
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
autoloader="tests/fixtures/stubs/class_alias.phpstub"
2018-11-29 06:05:56 +01:00
>
<projectFiles>
<directory name="src" />
</projectFiles>
2022-12-18 17:15:15 +01:00
</psalm>',
),
2018-11-29 06:05:56 +01:00
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace ClassAliasStubTest;
2018-11-29 06:05:56 +01:00
function foo(A $a) : void {}
foo(new B());
foo(new C());
2018-11-29 06:05:56 +01:00
function bar(B $b) : void {}
bar(new A());
$a = new B();
echo $a->foo;
echo $a->bar("hello");
function f(): A {
return new A;
}
function getAliased(): B {
return f();
}
$d = new D();
D::bat();
$d::bat();
2022-12-18 17:15:15 +01:00
class E implements IAlias {}',
2018-11-29 06:05:56 +01:00
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFunctionWithFunctionExists(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/custom_functions.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
function_exists("fooBar");
2022-12-18 17:15:15 +01:00
echo barBar("hello");',
);
$this->analyzeFile($file_path, new Context());
}
public function testNamespacedStubFunctionWithFunctionExists(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/custom_functions.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace A;
function_exists("fooBar");
2022-12-18 17:15:15 +01:00
echo barBar("hello");',
);
$this->analyzeFile($file_path, new Context());
}
public function testNoStubFunction(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('UndefinedFunction - /src/somefile.php:2:22 - Function barBar does not exist');
2021-12-03 20:29:06 +01:00
$this->expectException(CodeException::class);
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2022-12-18 17:15:15 +01:00
echo barBar("hello");',
);
$this->analyzeFile($file_path, new Context());
}
public function testNamespacedStubFunction(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/namespaced_functions.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2022-12-18 17:15:15 +01:00
echo Foo\barBar("hello");',
);
$this->analyzeFile($file_path, new Context());
}
public function testConditionalNamespacedStubFunction(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/conditional_namespaced_functions.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2022-12-18 17:15:15 +01:00
echo Foo\barBar("hello");',
);
$this->analyzeFile($file_path, new Context());
}
public function testConditionallyExtendingInterface(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/conditional_interface.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class C implements I1, I2, I3, I4 {}
function foo(I5 $d) : void {
$d->getMessage();
}
function bar(I6 $d) : void {
$d->getMessage();
}
function bat(I7 $d) : void {
$d->getMessage();
}
function baz(I8 $d) : void {
$d->getMessage();
2022-12-18 17:15:15 +01:00
}',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileWithExistingClassDefinition(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/DomainException.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2022-12-18 17:15:15 +01:00
$a = new DomainException(5);',
);
$this->analyzeFile($file_path, new Context());
}
/** @return iterable<string, array{string,string}> */
public function versionDependentStubsProvider(): iterable
{
yield '7.0' => [
'7.0',
'<?php
$a = new SomeClass;
2022-12-18 17:15:15 +01:00
$a->something("zzz");',
];
yield '8.0' => [
'8.0',
'<?php
$a = new SomeClass;
2022-12-18 17:15:15 +01:00
$a->something();',
];
}
/** @dataProvider versionDependentStubsProvider */
public function testVersionDependentStubs(string $php_version, string $code): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/VersionDependentMethods.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$this->project_analyzer->setPhpVersion($php_version, 'tests');
$file_path = getcwd() . '/src/somefile.php';
$this->addFile($file_path, $code);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileWithPartialClassDefinitionWithMoreMethods(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/partial_class.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Foo;
class PartiallyStubbedClass {
/**
* @param string $a
* @return object
*/
public function foo(string $a) {
return new self;
}
public function bar(int $i) : void {}
}
class A {}
(new PartiallyStubbedClass())->foo(A::class);
2022-12-18 17:15:15 +01:00
(new PartiallyStubbedClass())->bar(5);',
);
$this->analyzeFile($file_path, new Context());
}
public function testExtendOnlyStubbedClass(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/partial_class.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Foo;
class A extends PartiallyStubbedClass {}
2022-12-18 17:15:15 +01:00
(new A)->foo(A::class);',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileWithExtendedStubbedClass(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/partial_class.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Foo;
class Bar extends PartiallyStubbedClass {}
2022-12-18 17:15:15 +01:00
new Bar();',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileWithPartialClassDefinitionWithCoercion(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('TypeCoercion');
2021-12-03 20:29:06 +01:00
$this->expectException(CodeException::class);
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/partial_class.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Foo;
class PartiallyStubbedClass {
/**
* @param string $a
* @return object
*/
public function foo(string $a) {
return new self;
}
}
2022-12-18 17:15:15 +01:00
(new PartiallyStubbedClass())->foo("dasda");',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileWithPartialClassDefinitionGeneralReturnType(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('InvalidReturnStatement');
2021-12-03 20:29:06 +01:00
$this->expectException(CodeException::class);
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/partial_class.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Foo;
class PartiallyStubbedClass {
/**
* @param string $a
* @return object
*/
public function foo(string $a) {
return new \stdClass;
}
2022-12-18 17:15:15 +01:00
}',
);
$this->analyzeFile($file_path, new Context());
}
public function testStubFileWithTemplatedClassDefinitionAndMagicMethodOverride(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/templated_class.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class A {
/**
* @param int $id
* @param ?int $lockMode
* @param ?int $lockVersion
* @return mixed
*/
public function find($id, $lockMode = null, $lockVersion = null) {
return null;
}
}
/**
* @psalm-suppress MissingTemplateParam
*/
class B extends A {}
class Obj {}
/**
* @method ?Obj find(int $id, $lockMode = null, $lockVersion = null)
*/
2022-12-18 17:15:15 +01:00
class C extends B {}',
);
$this->analyzeFile($file_path, new Context());
}
public function testInheritedMethodUsedInStub(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm
findUnusedCode="true"
>
<projectFiles>
<directory name="src" />
</projectFiles>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$this->project_analyzer->getCodebase()->reportUnusedCode();
$vendor_file_path = getcwd() . '/vendor/vendor_class.php';
$this->addFile(
$vendor_file_path,
'<?php
namespace SomeVendor;
class VendorClass {
abstract public function foo() : void;
public static function vendorFunction(VendorClass $v) : void {
$v->foo();
}
2022-12-18 17:15:15 +01:00
}',
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class MyClass extends \SomeVendor\VendorClass {
public function foo() : void {}
}
2022-12-18 17:15:15 +01:00
\SomeVendor\VendorClass::vendorFunction(new MyClass);',
);
$this->analyzeFile($file_path, new Context(), false);
$this->project_analyzer->consolidateAnalyzedData();
}
public function testStubOverridingMissingClass(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/MissingClass.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
2020-09-14 04:39:03 +02:00
'<?php
2022-12-18 17:15:15 +01:00
echo "hello";',
);
2021-12-03 20:29:06 +01:00
$this->expectException(InvalidClasslikeOverrideException::class);
$this->analyzeFile($file_path, new Context());
}
public function testStubOverridingMissingMethod(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/MissingMethod.phpstub" />
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
2020-09-14 04:39:03 +02:00
'<?php
2022-12-18 17:15:15 +01:00
echo "hello";',
);
2021-12-03 20:29:06 +01:00
$this->expectException(InvalidMethodOverrideException::class);
$this->analyzeFile($file_path, new Context());
}
2020-12-01 16:26:45 +01:00
public function testStubReplacingInterfaceDocblock(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/fixtures/stubs/Doctrine.phpstub" />
2020-12-01 16:26:45 +01:00
</stubs>
2022-12-18 17:15:15 +01:00
</psalm>',
),
2020-12-01 16:26:45 +01:00
);
$this->addFile(
getcwd() . '/vendor/doctrine/import.php',
'<?php
namespace Doctrine\ORM;
interface EntityManagerInterface
{
/**
* @param string $entityName The name of the entity type.
* @param mixed $id The entity identifier.
*
* @return object|null The entity reference.
*/
public function getReference($entityName, $id);
}
class EntityManager implements EntityManagerInterface
{
/**
* @psalm-suppress InvalidReturnType
*/
public function getReference($entityName, $id) {
/**
* @psalm-suppress InvalidReturnStatement
*/
return new \stdClass;
}
2022-12-18 17:15:15 +01:00
}',
2020-12-01 16:26:45 +01:00
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
use Doctrine\ORM\EntityManager;
class A {}
2020-12-01 16:26:45 +01:00
function em(EntityManager $em) : void {
echo $em->getReference(A::class, 1);
2022-12-18 17:15:15 +01:00
}',
2020-12-01 16:26:45 +01:00
);
2021-12-03 20:29:06 +01:00
$this->expectException(CodeException::class);
$this->expectExceptionMessage('A|null');
2020-12-01 16:26:45 +01:00
$this->analyzeFile($file_path, new Context());
}
}