file_provider = new FakeFileProvider();
$this->original_error_handler = set_error_handler(null);
set_error_handler($this->original_error_handler);
}
private function getProjectAnalyzerWithConfig(Config $config): ProjectAnalyzer
{
$p = new ProjectAnalyzer(
$config,
new Providers(
$this->file_provider,
new FakeParserCacheProvider(),
),
);
$p->setPhpVersion('7.3', 'tests');
return $p;
}
public function testBarebonesConfig(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
}
public function testIgnoreProjectDirectory(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
}
public function testIgnoreMissingProjectDirectory(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertFalse($config->isInProjectDirs(realpath(__DIR__ . '/../../') . '/does/not/exist/FileAnalyzer.php'));
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
}
public function testIgnoreSymlinkedProjectDirectory(): void
{
@unlink(dirname(__DIR__, 1) . '/fixtures/symlinktest/ignored/b');
$no_symlinking_error = [
'symlink(): Cannot create symlink, error code(1314)',
'symlink(): Permission denied',
];
$last_error = error_get_last();
$check_symlink_error =
!is_array($last_error) ||
!isset($last_error['message']) ||
!in_array($last_error['message'], $no_symlinking_error);
@symlink(dirname(__DIR__, 1) . '/fixtures/symlinktest/a', dirname(__DIR__, 1) . '/fixtures/symlinktest/ignored/b');
if ($check_symlink_error) {
$last_error = error_get_last();
if (is_array($last_error) && in_array($last_error['message'], $no_symlinking_error)) {
$this->markTestSkipped($last_error['message']);
}
}
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('tests/AnnotationTest.php')));
$this->assertFalse($config->isInProjectDirs(realpath('tests/fixtures/symlinktest/a/ignoreme.php')));
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
$regex = '/^unlink\([^\)]+\): (?:Permission denied|No such file or directory)$/';
$last_error = error_get_last();
$check_unlink_error =
!is_array($last_error) ||
!preg_match($regex, $last_error['message']);
@unlink(__DIR__ . '/fixtures/symlinktest/ignored/b');
if ($check_unlink_error) {
$last_error = error_get_last();
if (is_array($last_error) && !preg_match($regex, $last_error['message'])) {
throw new ErrorException(
$last_error['message'],
0,
$last_error['type'],
$last_error['file'],
$last_error['line'],
);
}
}
}
public function testIgnoreWildcardProjectDirectory(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
}
public function testIgnoreRecursiveWildcardProjectDirectory(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Node/Expr/BinaryOp/VirtualPlus.php')));
}
public function testIgnoreRecursiveDoubleWildcardProjectFiles(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
}
public function testIgnoreWildcardFiles(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
}
public function testIgnoreWildcardFilesInWildcardFolder(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
$this->assertTrue($config->isInProjectDirs(realpath('examples/plugins/StringChecker.php')));
}
public function testIgnoreWildcardFilesInAllPossibleWildcardFolders(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
}
public function testIssueHandler(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath(__FILE__)));
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php')));
}
public function testReportMixedIssues(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertNull($config->show_mixed_issues);
$this->assertTrue($config->reportIssueInFile('MixedArgument', realpath(__FILE__)));
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertFalse($config->show_mixed_issues);
$this->assertFalse($config->reportIssueInFile('MixedArgument', realpath(__FILE__)));
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertNull($config->show_mixed_issues);
$this->assertFalse($config->reportIssueInFile('MixedArgument', realpath(__FILE__)));
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->show_mixed_issues);
$this->assertTrue($config->reportIssueInFile('MixedArgument', realpath(__FILE__)));
}
public function testGlobalUndefinedFunctionSuppression(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertSame(
Config::REPORT_SUPPRESS,
$config->getReportingLevelForFunction('UndefinedFunction', 'Some\Namespace\zzz'),
);
}
public function testMultipleIssueHandlers(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath(__FILE__)));
$this->assertFalse($config->reportIssueInFile('UndefinedClass', realpath(__FILE__)));
}
public function testIssueHandlerWithCustomErrorLevels(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Type.php'),
),
);
$this->assertSame(
'error',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'),
),
);
$this->assertSame(
'error',
$config->getReportingLevelForFile(
'PossiblyInvalidArgument',
realpath('src/psalm.php'),
),
);
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'PossiblyInvalidArgument',
realpath('examples/TemplateChecker.php'),
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\Badger',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\BadActor',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\GoodActor',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\MagicFactory',
),
);
$this->assertNull(
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\Bodger',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedMethod',
'Psalm\Bodger::find1',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedMethod',
'Psalm\Bodger::find2',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedMethod',
'Psalm\Badger::find2',
),
);
$this->assertNull(
$config->getReportingLevelForProperty(
'UndefinedMethod',
'Psalm\Bodger::$find3',
),
);
$this->assertNull(
$config->getReportingLevelForProperty(
'UndefinedMethod',
'Psalm\Bodger::$find4',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedFunction',
'fooBar',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedFunction',
'foobar',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForVariable(
'UndefinedGlobalVariable',
'a',
),
);
$this->assertNull(
$config->getReportingLevelForVariable(
'UndefinedGlobalVariable',
'b',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClassConstant(
'InvalidConstantAssignmentValue',
'Psalm\Bodger::FOO',
),
);
}
public function testIssueHandlerSetDynamically(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$config->setAdvancedErrorLevel('MissingReturnType', [
[
'type' => 'suppress',
'directory' => [['name' => 'tests']],
],
[
'type' => 'error',
'directory' => [['name' => 'src/Psalm/Internal/Analyzer']],
],
], 'info');
$config->setAdvancedErrorLevel('UndefinedClass', [
[
'type' => 'suppress',
'referencedClass' => [
['name' => 'Psalm\Badger'],
['name' => 'Psalm\*Actor'],
['name' => '*MagicFactory'],
],
],
]);
$config->setAdvancedErrorLevel('UndefinedMethod', [
[
'type' => 'suppress',
'referencedMethod' => [
['name' => 'Psalm\Bodger::find1'],
['name' => '*::find2'],
],
],
]);
$config->setAdvancedErrorLevel('UndefinedFunction', [
[
'type' => 'suppress',
'referencedFunction' => [
['name' => 'fooBar'],
],
],
]);
$config->setAdvancedErrorLevel('PossiblyInvalidArgument', [
[
'type' => 'suppress',
'directory' => [
['name' => 'tests'],
],
],
[
'type' => 'info',
'directory' => [
['name' => 'examples'],
],
],
]);
$config->setAdvancedErrorLevel('UndefinedPropertyFetch', [
[
'type' => 'suppress',
'referencedProperty' => [
['name' => 'Psalm\Bodger::$find3'],
],
],
]);
$config->setAdvancedErrorLevel('UndefinedGlobalVariable', [
[
'type' => 'suppress',
'referencedVariable' => [
['name' => 'a'],
],
],
]);
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Type.php'),
),
);
$this->assertSame(
'error',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'),
),
);
$this->assertSame(
'error',
$config->getReportingLevelForFile(
'PossiblyInvalidArgument',
realpath('src/psalm.php'),
),
);
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'PossiblyInvalidArgument',
realpath('examples/TemplateChecker.php'),
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\Badger',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\BadActor',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\GoodActor',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\MagicFactory',
),
);
$this->assertNull(
$config->getReportingLevelForClass(
'UndefinedClass',
'Psalm\Bodger',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedMethod',
'Psalm\Bodger::find1',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedMethod',
'Psalm\Bodger::find2',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedMethod',
'Psalm\Badger::find2',
),
);
$this->assertNull(
$config->getReportingLevelForProperty(
'UndefinedMethod',
'Psalm\Bodger::$find3',
),
);
$this->assertNull(
$config->getReportingLevelForProperty(
'UndefinedMethod',
'Psalm\Bodger::$find4',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedFunction',
'fooBar',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForMethod(
'UndefinedFunction',
'foobar',
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForVariable(
'UndefinedGlobalVariable',
'a',
),
);
$this->assertNull(
$config->getReportingLevelForVariable(
'UndefinedGlobalVariable',
'b',
),
);
}
public function testIssueHandlerOverride(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$config->setAdvancedErrorLevel('MissingReturnType', [
[
'type' => 'error',
'directory' => [['name' => 'src/Psalm/Internal/Analyzer']],
],
], 'info');
$config->setCustomErrorLevel('UndefinedClass', 'suppress');
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Type.php'),
),
);
$this->assertSame(
'error',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'),
),
);
$this->assertSame(
'suppress',
$config->getReportingLevelForFile(
'UndefinedClass',
realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'),
),
);
}
public function testIssueHandlerSafeOverride(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$config->safeSetAdvancedErrorLevel('MissingReturnType', [
[
'type' => 'error',
'directory' => [['name' => 'src/Psalm/Internal/Analyzer']],
],
], 'info');
$config->safeSetCustomErrorLevel('UndefinedClass', 'suppress');
$this->assertSame(
'error',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Type.php'),
),
);
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'),
),
);
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'UndefinedClass',
realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'),
),
);
}
public function testAllPossibleIssues(): void
{
$all_possible_handlers = implode(
' ',
array_map(
/**
* @param string $issue_name
* @return string
*/
static fn($issue_name): string => '<' . $issue_name . ' errorLevel="suppress" />' . "\n",
IssueHandler::getAllIssueTypes(),
),
);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
' . $all_possible_handlers . '
',
),
);
}
public function testImpossibleIssue(): void
{
$this->expectExceptionMessage('This element is not expected');
$this->expectException(ConfigException::class);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
}
public function testThing(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'foo($b = 5);
echo $b;',
);
$this->analyzeFile($file_path, new Context());
}
public function testValidThrowInvalidCatch(): void
{
$this->expectExceptionMessage('InvalidCatch');
$this->expectException(CodeException::class);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'analyzeFile($file_path, new Context());
}
public function testInvalidThrowValidCatch(): void
{
$this->expectExceptionMessage('InvalidThrow');
$this->expectException(CodeException::class);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'analyzeFile($file_path, new Context());
}
public function testValidThrowValidCatch(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'analyzeFile($file_path, new Context());
}
public function testModularConfig(): void
{
$root = __DIR__ . '/../fixtures/ModularConfig';
$config = Config::loadFromXMLFile($root . '/psalm.xml', $root);
$this->assertEquals(
[
realpath($root . '/Bar.php'),
realpath($root . '/Bat.php'),
],
$config->getProjectFiles(),
);
}
public function tearDown(): void
{
set_error_handler($this->original_error_handler);
parent::tearDown();
if ($this->getName() === 'testTemplatedFiles') {
$project_root = dirname(__DIR__, 2);
foreach (['1.xml', '2.xml', '3.xml', '4.xml', '5.xml', '6.xml', '7.xml', '8.xml'] as $file_name) {
@unlink($project_root . DIRECTORY_SEPARATOR . $file_name);
}
}
}
public function testGlobals(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'func();
ord($_GET["str"]);
assert($glob4 !== null);
ord($glob4);
function example1(): void {
global $glob1, $glob2, $glob3, $glob4;
ord($glob1);
ord($glob2["str"]);
$glob3->func();
ord($glob4);
ord($_GET["str"]);
}
$z = $glob1;
$z = 0;
error_reporting($z);
$old = $_GET["str"];
$_GET["str"] = 0;
error_reporting($_GET["str"]);
$_GET["str"] = $old;
function example2(): void {
global $z, $glob2, $glob3;
error_reporting($z);
ord($glob2["str"]);
$glob3->func();
ord($_GET["str"]);
}
}
namespace ns {
ord($glob1);
ord($glob2["str"]);
$glob3->func();
ord($_GET["str"]);
class Clazz {
public function func(): void {}
}
function example3(): void {
global $glob1, $glob2, $glob3;
ord($glob1);
ord($glob2["str"]);
$glob3->func();
ord($_GET["str"]);
}
}
namespace ns2 {
/** @psalm-suppress InvalidGlobal */
global $glob1, $glob2, $glob3;
ord($glob1);
ord($glob2["str"]);
$glob3->func();
}
namespace {
ord($glob1 ?: "str");
ord($_GET["str"] ?? "str");
function example4(): void {
global $glob1;
ord($glob1 ?: "str");
ord($_GET["str"] ?? "str");
}
}',
);
$this->analyzeFile($file_path, new Context());
}
public function testIgnoreExceptions(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'analyzeFile($file_path, new Context());
}
public function testNotIgnoredException(): void
{
$this->expectException(CodeException::class);
$this->expectExceptionMessage('MissingThrowsDocblock');
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'analyzeFile($file_path, new Context());
}
public function testGetPossiblePsr4Path(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$classloader = new ClassLoader();
$classloader->addPsr4(
'Psalm\\',
[
dirname(__DIR__, 2)
. DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
. 'src' . DIRECTORY_SEPARATOR . 'Psalm',
],
);
$classloader->addPsr4(
'Psalm\\Tests\\',
[
dirname(__DIR__, 2)
. DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
. 'tests',
],
);
$config->setComposerClassLoader($classloader);
$this->assertSame(
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Psalm' . DIRECTORY_SEPARATOR . 'Foo.php',
$config->getPotentialComposerFilePathForClassLike('Psalm\\Foo'),
);
$this->assertSame(
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'Foo.php',
$config->getPotentialComposerFilePathForClassLike('Psalm\\Tests\\Foo'),
);
}
public function testTakesPhpVersionFromConfigFile(): void
{
$cfg = Config::loadFromXML(
dirname(__DIR__, 2),
'',
);
$this->assertSame('7.1', $cfg->getPhpVersion());
}
public function testReadsComposerJsonForPhpVersion(): void
{
$root = __DIR__ . '/../fixtures/ComposerPhpVersion';
$cfg = Config::loadFromXML($root, "");
$this->assertSame('7.2', $cfg->getPhpVersion());
$cfg = Config::loadFromXML($root, "");
$this->assertSame('8.0', $cfg->getPhpVersion());
}
public function testSetsUsePhpStormMetaPath(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$this->assertFalse($this->project_analyzer->getConfig()->use_phpstorm_meta_path);
}
public function testSetsUniversalObjectCrates(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$this->assertContains('datetime', $this->project_analyzer->getConfig()->getUniversalObjectCrates());
}
public function testInferPropertyTypesFromConstructorIsRead(): void
{
$cfg = Config::loadFromXML(
dirname(__DIR__, 2),
'',
);
$this->assertFalse($cfg->infer_property_types_from_constructor);
}
/**
* @return array
*/
public function pluginRegistersScannerAndAnalyzerDataProvider(): array
{
return [
'regular' => [0, null], // flags, expected exception code
'invalid scanner class' => [FileTypeSelfRegisteringPlugin::FLAG_SCANNER_INVALID, 1_622_727_271],
'invalid analyzer class' => [FileTypeSelfRegisteringPlugin::FLAG_ANALYZER_INVALID, 1_622_727_281],
'override scanner' => [FileTypeSelfRegisteringPlugin::FLAG_SCANNER_TWICE, 1_622_727_272],
'override analyzer' => [FileTypeSelfRegisteringPlugin::FLAG_ANALYZER_TWICE, 1_622_727_282],
];
}
/**
* @test
* @dataProvider pluginRegistersScannerAndAnalyzerDataProvider
*/
public function pluginRegistersScannerAndAnalyzer(int $flags, ?int $expectedExceptionCode): void
{
$extension = uniqid('test');
$names = [
'scanner' => uniqid('PsalmTestFileTypeScanner'),
'analyzer' => uniqid('PsalmTestFileTypeAnalyzer'),
'extension' => $extension,
];
$scannerMock = $this->getMockBuilder(FileScanner::class)
->setMockClassName($names['scanner'])
->disableOriginalConstructor()
->getMock();
$analyzerMock = $this->getMockBuilder(FileAnalyzer::class)
->setMockClassName($names['analyzer'])
->disableOriginalConstructor()
->getMock();
FileTypeSelfRegisteringPlugin::$names = $names;
FileTypeSelfRegisteringPlugin::$flags = $flags;
$xml = sprintf(
'
',
FileTypeSelfRegisteringPlugin::class,
);
try {
$projectAnalyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(dirname(__DIR__, 2), $xml),
);
$config = $projectAnalyzer->getConfig();
$config->initializePlugins($projectAnalyzer);
} catch (ConfigException $exception) {
$actualExceptionCode = $exception->getPrevious()
? $exception->getPrevious()->getCode()
: null;
self::assertSame(
$expectedExceptionCode,
$actualExceptionCode,
'Exception code did not match.',
);
return;
}
self::assertContains($extension, $config->getFileExtensions());
self::assertSame(get_class($scannerMock), $config->getFiletypeScanners()[$extension] ?? null);
self::assertSame(get_class($analyzerMock), $config->getFiletypeAnalyzers()[$extension] ?? null);
self::assertNull($expectedExceptionCode, 'Expected exception code was not thrown');
}
public function testTypeStatsForFileReporting(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
(string) getcwd(),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertFalse($config->reportTypeStatsForFile(realpath('src/Psalm/Config') . DIRECTORY_SEPARATOR));
$this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Internal') . DIRECTORY_SEPARATOR));
$this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Issue') . DIRECTORY_SEPARATOR));
$this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Node') . DIRECTORY_SEPARATOR));
$this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Plugin') . DIRECTORY_SEPARATOR));
$this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Progress') . DIRECTORY_SEPARATOR));
$this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Report') . DIRECTORY_SEPARATOR));
$this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/SourceControl') . DIRECTORY_SEPARATOR));
}
public function testStrictTypesForFileReporting(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
(string) getcwd(),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->useStrictTypesForFile(realpath('src/Psalm/Config') . DIRECTORY_SEPARATOR));
$this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Internal') . DIRECTORY_SEPARATOR));
$this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Issue') . DIRECTORY_SEPARATOR));
$this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Node') . DIRECTORY_SEPARATOR));
$this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Plugin') . DIRECTORY_SEPARATOR));
$this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Progress') . DIRECTORY_SEPARATOR));
$this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Report') . DIRECTORY_SEPARATOR));
$this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/SourceControl') . DIRECTORY_SEPARATOR));
}
public function testConfigFileWithXIncludeWithoutFallbackShouldThrowException(): void
{
$this->expectException(ConfigException::class);
$this->expectExceptionMessageMatches('/and no fallback was found/');
ErrorHandler::install();
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
);
}
public function testConfigFileWithXIncludeWithFallback(): void
{
ErrorHandler::install();
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertFalse($config->reportIssueInFile('MixedAssignment', realpath('src/Psalm/Type.php')));
}
public function testConfigFileWithWildcardPathIssueHandler(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'
',
),
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->reportIssueInFile('MissingReturnType', realpath(__FILE__)));
$this->assertTrue($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php')));
$this->assertTrue($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Node/Expr/BinaryOp/VirtualPlus.php')));
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php')));
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Type/TypeAlias.php')));
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php')));
}
/**
* @requires extension apcu
* @deprecated Remove in Psalm 6.
*/
public function testConfigWarnsAboutDeprecatedWayToLoadStubsButLoadsTheStub(): void
{
$config_xml = Config::loadFromXML(
(string)getcwd(),
'
',
);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig($config_xml);
$codebase = $this->project_analyzer->getCodebase();
$config = $this->project_analyzer->getConfig();
$config->visitStubFiles($codebase);
$this->assertContains(realpath('stubs/extensions/apcu.phpstub'), $config->internal_stubs);
$this->assertContains(
'Psalm 6 will not automatically load stubs for ext-apcu. You should explicitly enable or disable this ext in composer.json or Psalm config.',
$config->config_warnings,
);
}
/**
* @requires extension apcu
* @deprecated Remove deprecation warning part in Psalm 6.
*/
public function testConfigWithDisableExtensionsDoesNotLoadExtensionStubsAndHidesDeprecationWarning(): void
{
$config_xml = Config::loadFromXML(
(string)getcwd(),
'
',
);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig($config_xml);
$codebase = $this->project_analyzer->getCodebase();
$config = $this->project_analyzer->getConfig();
$config->visitStubFiles($codebase);
$this->assertNotContains(realpath('stubs/extensions/apcu.phpstub'), $config->internal_stubs);
$this->assertNotContains(
'Psalm 6 will not automatically load stubs for ext-apcu. You should explicitly enable or disable this ext in composer.json or Psalm config.',
$config->internal_stubs,
);
}
public function testReferencedFunctionAllowsMethods(): void
{
$config_xml = Config::loadFromXML(
(string) getcwd(),
<<
XML,
);
$this->assertSame(
Config::REPORT_SUPPRESS,
$config_xml->getReportingLevelForIssue(
new TooManyArguments(
'too many',
new Raw('aaa', 'aaa.php', 'aaa.php', 1, 2),
'Foo\Bar::baZ',
),
),
);
}
public function testReferencedFunctionAllowsNamespacedFunctions(): void
{
$config_xml = Config::loadFromXML(
(string) getcwd(),
<<
XML,
);
$this->assertSame(
Config::REPORT_SUPPRESS,
$config_xml->getReportingLevelForIssue(
new UndefinedFunction(
'Function Foo\Bar\baz does not exist',
new Raw('aaa', 'aaa.php', 'aaa.php', 1, 2),
'foo\bar\baz',
),
),
);
}
}