mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
d19aad7db1
Historically it was often not quite clear to users what PHP version Psalm assumes, and why. This PR addresses this issue by printing the version and where we got it from right before scanning the files.
1298 lines
37 KiB
PHP
1298 lines
37 KiB
PHP
<?php
|
|
namespace Psalm\Tests;
|
|
|
|
use Psalm\Config;
|
|
use Psalm\Context;
|
|
use Psalm\Internal\IncludeCollector;
|
|
use Psalm\Internal\Provider\FakeFileProvider;
|
|
use Psalm\Internal\RuntimeCaches;
|
|
use Psalm\Tests\Internal\Provider;
|
|
|
|
use function define;
|
|
use function defined;
|
|
use function dirname;
|
|
use function explode;
|
|
use function getcwd;
|
|
use function implode;
|
|
|
|
use const DIRECTORY_SEPARATOR;
|
|
|
|
class StubTest extends TestCase
|
|
{
|
|
/** @var TestConfig */
|
|
protected static $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
|
|
{
|
|
RuntimeCaches::clearAll();
|
|
$this->file_provider = new FakeFileProvider();
|
|
}
|
|
|
|
private function getProjectAnalyzerWithConfig(Config $config): \Psalm\Internal\Analyzer\ProjectAnalyzer
|
|
{
|
|
$project_analyzer = new \Psalm\Internal\Analyzer\ProjectAnalyzer(
|
|
$config,
|
|
new \Psalm\Internal\Provider\Providers(
|
|
$this->file_provider,
|
|
new Provider\FakeParserCacheProvider()
|
|
)
|
|
);
|
|
$project_analyzer->setPhpVersion('7.4', 'tests');
|
|
|
|
$config->setIncludeCollector(new IncludeCollector());
|
|
$config->visitComposerAutoloadFiles($project_analyzer, null);
|
|
|
|
return $project_analyzer;
|
|
}
|
|
|
|
public function testNonexistentStubFile(): void
|
|
{
|
|
$this->expectException(\Psalm\Exception\ConfigException::class);
|
|
$this->expectExceptionMessage('Cannot resolve stubfile path');
|
|
$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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
}
|
|
|
|
public function testStubFileClass(): 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>
|
|
</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");
|
|
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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$path = $this->getOperatingSystemStyledPath('tests/fixtures/stubs/systemclass.phpstub');
|
|
$stub_files = $this->project_analyzer->getConfig()->getStubFiles();
|
|
$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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$path = $this->getOperatingSystemStyledPath('tests/fixtures/stubs/systemclass.phpstub');
|
|
$stub_files = $this->project_analyzer->getConfig()->getStubFiles();
|
|
$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>
|
|
</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;
|
|
$g = \ROOT_DEFINE_CONSTANT;'
|
|
);
|
|
|
|
$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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
namespace Ns {
|
|
class MyClass {
|
|
/**
|
|
* @return mixed
|
|
* @psalm-suppress InvalidReturnType
|
|
*/
|
|
public function create(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) {}
|
|
|
|
/**
|
|
* @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");
|
|
|
|
$b1 = \Create("object");
|
|
$b2 = \cReate("exception");
|
|
|
|
$e2 = \creAte(\LogicException::class);
|
|
|
|
$c1 = (new \Ns\MyClass)->foo(5);
|
|
$c2 = (new \Ns\MyClass)->bar(["hello"]);
|
|
|
|
$d1 = \foO(5);
|
|
$d2 = \baR(["hello"]);
|
|
}'
|
|
);
|
|
|
|
$context = new Context();
|
|
$this->analyzeFile($file_path, $context);
|
|
}
|
|
|
|
public function testNamespacedStubClass(): 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/namespaced_class.phpstub" />
|
|
</stubs>
|
|
</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");
|
|
|
|
echo Foo\BAR;'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testStubRegularFunction(): 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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
variadic("bat", "bam");'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testStubVariadicFunctionWrongArgType(): void
|
|
{
|
|
$this->expectExceptionMessage('InvalidScalarArgument');
|
|
$this->expectException(\Psalm\Exception\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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
variadic("bat", 5);'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testUserVariadicWithFalseVariadic(): void
|
|
{
|
|
$this->expectExceptionMessage('TooManyArguments');
|
|
$this->expectException(\Psalm\Exception\CodeException::class);
|
|
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
|
|
TestConfig::loadFromXML(
|
|
dirname(__DIR__),
|
|
'<?xml version="1.0"?>
|
|
<psalm
|
|
errorLevel="1"
|
|
>
|
|
<projectFiles>
|
|
<directory name="src" />
|
|
</projectFiles>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
/**
|
|
* @param string ...$bar
|
|
*/
|
|
function variadic() : void {}
|
|
variadic("hello");'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testPolyfilledFunction(): void
|
|
{
|
|
$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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
$a = random_bytes(16);
|
|
$b = new_random_bytes(16);'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
/**
|
|
* @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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
/** @psalm-suppress MixedArgument */
|
|
echo CODE_DIR;'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testClassAlias(): void
|
|
{
|
|
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
|
|
TestConfig::loadFromXML(
|
|
dirname(__DIR__),
|
|
'<?xml version="1.0"?>
|
|
<psalm
|
|
errorLevel="1"
|
|
autoloader="tests/fixtures/stubs/class_alias.phpstub"
|
|
>
|
|
<projectFiles>
|
|
<directory name="src" />
|
|
</projectFiles>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
namespace ClassAliasStubTest;
|
|
|
|
function foo(A $a) : void {}
|
|
|
|
foo(new B());
|
|
foo(new C());
|
|
|
|
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();
|
|
|
|
class E implements IAlias {}'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testStubFunctionWithFunctionExists(): 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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
function_exists("fooBar");
|
|
echo barBar("hello");'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testNamespacedStubFunctionWithFunctionExists(): 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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
namespace A;
|
|
function_exists("fooBar");
|
|
echo barBar("hello");'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testNoStubFunction(): void
|
|
{
|
|
$this->expectExceptionMessage('UndefinedFunction - /src/somefile.php:2:22 - Function barBar does not exist');
|
|
$this->expectException(\Psalm\Exception\CodeException::class);
|
|
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
|
|
TestConfig::loadFromXML(
|
|
dirname(__DIR__),
|
|
'<?xml version="1.0"?>
|
|
<psalm
|
|
errorLevel="1"
|
|
>
|
|
<projectFiles>
|
|
<directory name="src" />
|
|
</projectFiles>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
echo barBar("hello");'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testNamespacedStubFunction(): 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/namespaced_functions.phpstub" />
|
|
</stubs>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
echo Foo\barBar("hello");'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testConditionalNamespacedStubFunction(): 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_namespaced_functions.phpstub" />
|
|
</stubs>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
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>
|
|
</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();
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testStubFileWithExistingClassDefinition(): 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/DomainException.phpstub" />
|
|
</stubs>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
$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;
|
|
$a->something("zzz");'
|
|
];
|
|
yield '8.0' => [
|
|
'8.0',
|
|
'<?php
|
|
$a = new SomeClass;
|
|
$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>
|
|
</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
|
|
{
|
|
$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>
|
|
</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);
|
|
(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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
namespace Foo;
|
|
|
|
class A extends PartiallyStubbedClass {}
|
|
|
|
(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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
namespace Foo;
|
|
|
|
class Bar extends PartiallyStubbedClass {}
|
|
|
|
new Bar();'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testStubFileWithPartialClassDefinitionWithCoercion(): void
|
|
{
|
|
$this->expectExceptionMessage('TypeCoercion');
|
|
$this->expectException(\Psalm\Exception\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/partial_class.phpstub" />
|
|
</stubs>
|
|
</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;
|
|
}
|
|
}
|
|
|
|
(new PartiallyStubbedClass())->foo("dasda");'
|
|
);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
public function testStubFileWithPartialClassDefinitionGeneralReturnType(): void
|
|
{
|
|
$this->expectExceptionMessage('InvalidReturnStatement');
|
|
$this->expectException(\Psalm\Exception\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/partial_class.phpstub" />
|
|
</stubs>
|
|
</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;
|
|
}
|
|
}'
|
|
);
|
|
|
|
$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>
|
|
</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) {}
|
|
}
|
|
|
|
class B extends A {}
|
|
|
|
class Obj {}
|
|
|
|
/**
|
|
* @method ?Obj find(int $id, $lockMode = null, $lockVersion = null)
|
|
*/
|
|
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>
|
|
</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();
|
|
}
|
|
}'
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
class MyClass extends \SomeVendor\VendorClass {
|
|
public function foo() : void {}
|
|
}
|
|
|
|
\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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
|
|
echo "hello";'
|
|
);
|
|
|
|
$this->expectException(\Psalm\Exception\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>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
|
|
echo "hello";'
|
|
);
|
|
|
|
$this->expectException(\Psalm\Exception\InvalidMethodOverrideException::class);
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
|
|
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" />
|
|
</stubs>
|
|
</psalm>'
|
|
)
|
|
);
|
|
|
|
$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;
|
|
}
|
|
}'
|
|
);
|
|
|
|
$file_path = getcwd() . '/src/somefile.php';
|
|
|
|
$this->addFile(
|
|
$file_path,
|
|
'<?php
|
|
use Doctrine\ORM\EntityManager;
|
|
|
|
class A {}
|
|
|
|
function em(EntityManager $em) : void {
|
|
echo $em->getReference(A::class, 1);
|
|
}'
|
|
);
|
|
|
|
$this->expectException(\Psalm\Exception\CodeException::class);
|
|
$this->expectExceptionMessage('A|null');
|
|
|
|
$this->analyzeFile($file_path, new Context());
|
|
}
|
|
}
|