1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 12:55:26 +01:00
psalm/tests/Config/ConfigTest.php

1486 lines
48 KiB
PHP
Raw Normal View History

2016-12-29 16:24:10 +01:00
<?php
2019-05-10 00:58:30 +02:00
namespace Psalm\Tests\Config;
2016-12-29 16:24:10 +01:00
2021-12-03 20:11:20 +01:00
use Composer\Autoload\ClassLoader;
2021-12-03 21:40:18 +01:00
use ErrorException;
2021-06-08 04:55:21 +02:00
use Psalm\Config;
2021-12-03 20:11:20 +01:00
use Psalm\Config\IssueHandler;
2021-06-08 04:55:21 +02:00
use Psalm\Context;
2021-12-03 20:29:06 +01:00
use Psalm\Exception\CodeException;
use Psalm\Exception\ConfigException;
use Psalm\Internal\Analyzer\FileAnalyzer;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Analyzer\ProjectAnalyzer;
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;
use Psalm\Internal\Scanner\FileScanner;
use Psalm\Tests\Config\Plugin\FileTypeSelfRegisteringPlugin;
2021-06-08 04:55:21 +02:00
use Psalm\Tests\Internal\Provider;
2021-12-03 20:11:20 +01:00
use Psalm\Tests\TestCase;
2021-06-08 04:55:21 +02:00
use Psalm\Tests\TestConfig;
2019-07-05 22:24:00 +02:00
use function array_map;
use function define;
use function defined;
use function dirname;
use function error_get_last;
2021-06-08 04:55:21 +02:00
use function get_class;
2019-07-05 22:24:00 +02:00
use function getcwd;
use function implode;
2021-11-28 00:18:13 +01:00
use function in_array;
2019-07-05 22:24:00 +02:00
use function is_array;
use function preg_match;
use function realpath;
2021-06-08 04:55:21 +02:00
use function sprintf;
use function symlink;
2021-06-08 04:55:21 +02:00
use function uniqid;
2019-07-05 22:24:00 +02:00
use function unlink;
2016-12-29 16:24:10 +01:00
2021-06-08 04:55:21 +02:00
use const DIRECTORY_SEPARATOR;
2021-12-03 20:11:20 +01:00
class ConfigTest extends TestCase
2016-12-29 16:24:10 +01:00
{
/** @var TestConfig */
protected static $config;
2018-11-06 03:57:36 +01:00
/** @var \Psalm\Internal\Analyzer\ProjectAnalyzer */
2018-11-11 18:01:14 +01:00
protected $project_analyzer;
2019-05-17 00:36:36 +02:00
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');
}
}
2019-05-17 00:36:36 +02:00
public function setUp() : void
2016-12-29 16:24:10 +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
RuntimeCaches::clearAll();
$this->file_provider = new FakeFileProvider();
2016-12-29 16:24:10 +01:00
}
2021-12-03 20:11:20 +01:00
private function getProjectAnalyzerWithConfig(Config $config): ProjectAnalyzer
{
2021-12-03 20:11:20 +01:00
$p = new ProjectAnalyzer(
$config,
2021-12-03 20:11:20 +01:00
new Providers(
$this->file_provider,
new Provider\FakeParserCacheProvider()
)
);
2019-02-07 21:27:43 +01:00
$p->setPhpVersion('7.3', 'tests');
2019-02-07 21:27:43 +01:00
return $p;
}
public function testBarebonesConfig(): void
2016-12-29 16:24:10 +01:00
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
</psalm>'
)
);
2016-12-29 16:24:10 +01:00
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
2018-11-06 03:57:36 +01:00
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
2016-12-29 16:24:10 +01:00
}
public function testIgnoreProjectDirectory(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<ignoreFiles>
2018-11-06 03:57:36 +01:00
<directory name="src/Psalm/Internal/Analyzer" />
</ignoreFiles>
</projectFiles>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
2018-11-06 03:57:36 +01:00
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
}
2016-12-30 02:07:42 +01:00
public function testIgnoreMissingProjectDirectory(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<ignoreFiles allowMissingFiles="true">
<directory name="does/not/exist" />
</ignoreFiles>
</projectFiles>
</psalm>'
)
);
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
$this->assertFalse($config->isInProjectDirs(realpath('does/not/exist/FileAnalyzer.php')));
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
}
public function testIgnoreSymlinkedProjectDirectory(): void
{
2019-05-10 00:58:30 +02:00
@unlink(dirname(__DIR__, 1) . '/fixtures/symlinktest/ignored/b');
2021-11-28 00:18:13 +01:00
$no_symlinking_error = [
'symlink(): Cannot create symlink, error code(1314)',
'symlink(): Permission denied',
];
$last_error = error_get_last();
$check_symlink_error =
2019-03-23 19:27:54 +01:00
!is_array($last_error) ||
!isset($last_error['message']) ||
2021-11-28 00:18:13 +01:00
!in_array($last_error['message'], $no_symlinking_error);
2019-05-10 00:58:30 +02:00
@symlink(dirname(__DIR__, 1) . '/fixtures/symlinktest/a', dirname(__DIR__, 1) . '/fixtures/symlinktest/ignored/b');
if ($check_symlink_error) {
$last_error = error_get_last();
2021-11-28 00:18:13 +01:00
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(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests" />
<ignoreFiles>
<directory name="tests/fixtures/symlinktest/ignored" />
</ignoreFiles>
</projectFiles>
</psalm>'
)
);
$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/StringAnalyzer.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'])) {
2021-12-03 21:40:18 +01:00
throw new ErrorException(
$last_error['message'],
0,
$last_error['type'],
$last_error['file'],
$last_error['line']
);
}
}
}
public function testIgnoreWildcardProjectDirectory(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<ignoreFiles>
2018-11-06 03:57:36 +01:00
<directory name="src/**/Internal/Analyzer" />
</ignoreFiles>
</projectFiles>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
2018-11-06 03:57:36 +01:00
$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/StringAnalyzer.php')));
}
public function testIgnoreWildcardFiles(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<ignoreFiles>
2018-11-06 03:57:36 +01:00
<file name="src/Psalm/Internal/Analyzer/*Analyzer.php" />
</ignoreFiles>
</projectFiles>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
2018-11-06 03:57:36 +01:00
$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/StringAnalyzer.php')));
}
public function testIgnoreWildcardFilesInWildcardFolder(): void
2018-07-22 04:34:22 +02:00
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
2018-07-22 04:34:22 +02:00
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
2018-07-22 04:34:22 +02:00
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<directory name="examples" />
<ignoreFiles>
2018-11-06 03:57:36 +01:00
<file name="src/Psalm/**/**/*Analyzer.php" />
<file name="src/Psalm/**/**/**/*Analyzer.php" />
2018-07-22 04:34:22 +02:00
</ignoreFiles>
</projectFiles>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
2018-07-22 04:34:22 +02:00
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
2020-03-15 04:54:42 +01:00
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php')));
2018-11-06 03:57:36 +01:00
$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')));
2018-07-22 04:34:22 +02:00
}
public function testIgnoreWildcardFilesInAllPossibleWildcardFolders(): void
2018-07-22 04:34:22 +02:00
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
2018-07-22 04:34:22 +02:00
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
2018-07-22 04:34:22 +02:00
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<directory name="examples" />
<ignoreFiles>
2018-11-06 03:57:36 +01:00
<file name="**/**/**/**/*Analyzer.php" />
<file name="**/**/**/**/**/*Analyzer.php" />
2018-07-22 04:34:22 +02:00
</ignoreFiles>
</projectFiles>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
2018-07-22 04:34:22 +02:00
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
2020-03-15 04:54:42 +01:00
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php')));
2018-11-06 03:57:36 +01:00
$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/StringAnalyzer.php')));
2018-07-22 04:34:22 +02:00
}
public function testIssueHandler(): void
2016-12-30 02:07:42 +01:00
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<directory name="tests" />
</projectFiles>
<issueHandlers>
<MissingReturnType errorLevel="suppress" />
</issueHandlers>
</psalm>'
)
);
2016-12-30 02:07:42 +01:00
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('tests/ConfigTest.php')));
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php')));
2016-12-30 02:07:42 +01:00
}
public function testIssueHandlerWithCustomErrorLevels(): void
2016-12-30 02:07:42 +01:00
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<directory name="tests" />
</projectFiles>
<issueHandlers>
<MissingReturnType errorLevel="info">
<errorLevel type="suppress">
<directory name="tests" />
</errorLevel>
<errorLevel type="error">
2018-11-06 03:57:36 +01:00
<directory name="src/Psalm/Internal/Analyzer" />
</errorLevel>
</MissingReturnType>
<UndefinedClass>
<errorLevel type="suppress">
<referencedClass name="Psalm\Badger" />
<referencedClass name="Psalm\*Actor" />
<referencedClass name="*MagicFactory" />
</errorLevel>
</UndefinedClass>
<UndefinedMethod>
<errorLevel type="suppress">
<referencedMethod name="Psalm\Bodger::find1" />
<referencedMethod name="*::find2" />
</errorLevel>
</UndefinedMethod>
<UndefinedFunction>
<errorLevel type="suppress">
<referencedFunction name="fooBar" />
</errorLevel>
2019-05-03 23:12:20 +02:00
<errorLevel type="info">
<directory name="examples" />
</errorLevel>
</UndefinedFunction>
2019-05-03 23:12:20 +02:00
<PossiblyInvalidArgument>
<errorLevel type="suppress">
<directory name="tests" />
</errorLevel>
<errorLevel type="info">
<directory name="examples" />
</errorLevel>
</PossiblyInvalidArgument>
<UndefinedPropertyFetch>
<errorLevel type="suppress">
<referencedProperty name="Psalm\Bodger::$find3" />
</errorLevel>
</UndefinedPropertyFetch>
<UndefinedGlobalVariable>
<errorLevel type="suppress">
<referencedVariable name="a" />
</errorLevel>
</UndefinedGlobalVariable>
</issueHandlers>
</psalm>'
)
);
2016-12-30 02:07:42 +01:00
2018-11-11 18:01:14 +01:00
$config = $this->project_analyzer->getConfig();
$this->assertSame(
'info',
$config->getReportingLevelForFile(
'MissingReturnType',
realpath('src/Psalm/Type.php')
)
);
$this->assertSame(
'error',
$config->getReportingLevelForFile(
'MissingReturnType',
2018-11-06 03:57:36 +01:00
realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')
)
);
2019-05-03 23:12:20 +02:00
$this->assertSame(
'error',
$config->getReportingLevelForFile(
2021-09-23 17:30:30 +02:00
'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 testIssueHandlerSetDynamically(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<directory name="tests" />
</projectFiles>
</psalm>'
)
);
$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(
2019-05-03 23:12:20 +02:00
'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'
)
);
2019-07-05 22:24:00 +02:00
$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'
)
);
2019-07-05 22:24:00 +02:00
$this->assertNull(
$config->getReportingLevelForProperty(
'UndefinedMethod',
'Psalm\Bodger::$find3'
)
);
2019-07-05 22:24:00 +02:00
$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'
)
);
2016-12-30 02:07:42 +01:00
}
public function testAllPossibleIssues(): void
{
$all_possible_handlers = implode(
' ',
array_map(
2017-01-13 20:14:24 +01:00
/**
* @param string $issue_name
2017-05-27 02:16:18 +02:00
*
2017-01-13 20:14:24 +01:00
* @return string
*/
function ($issue_name): string {
return '<' . $issue_name . ' errorLevel="suppress" />' . "\n";
},
2021-12-03 20:11:20 +01:00
IssueHandler::getAllIssueTypes()
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<issueHandlers>
' . $all_possible_handlers . '
</issueHandlers>
</psalm>'
)
);
}
public function testImpossibleIssue(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('This element is not expected');
$this->expectException(ConfigException::class);
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<issueHandlers>
<ImpossibleIssue errorLevel="suppress" />
</issueHandlers>
</psalm>'
)
);
}
public function testThing(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<mockClasses>
<class name="MyMockClass" />
</mockClasses>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class MyMockClass {}
$a = new MyMockClass();
$a->foo($b = 5);
echo $b;'
);
$this->analyzeFile($file_path, new Context());
}
public function testExitFunctions(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<exitFunctions>
<function name="leave" />
<function name="Foo\namespacedLeave" />
<function name="Foo\Bar::staticLeave" />
</exitFunctions>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace {
function leave() : void {
exit();
}
function mightLeave() : string {
if (rand(0, 1)) {
leave();
} else {
return "here";
}
}
function mightLeaveWithNamespacedFunction() : string {
if (rand(0, 1)) {
\Foo\namespacedLeave();
} else {
return "here";
}
}
function mightLeaveWithStaticMethod() : string {
if (rand(0, 1)) {
Foo\Bar::staticLeave();
} else {
return "here";
}
}
}
namespace Foo {
function namespacedLeave() : void {
exit();
}
class Bar {
public static function staticLeave() : void {
exit();
}
}
}'
);
$this->analyzeFile($file_path, new Context());
}
public function testValidThrowInvalidCatch(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('InvalidCatch');
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(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<issueHandlers>
<InvalidThrow>
<errorLevel type="suppress">
<referencedClass name="I" />
</errorLevel>
</InvalidThrow>
</issueHandlers>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
interface I {}
class E extends Exception implements I {}
function foo() : void {
throw new E();
}
function handleThrow(I $e) : void {
echo "about to throw";
throw $e;
}
try {
foo();
} catch (I $e) {
handleThrow($e);
}'
);
$this->analyzeFile($file_path, new Context());
}
public function testInvalidThrowValidCatch(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('InvalidThrow');
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(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<issueHandlers>
<InvalidCatch>
<errorLevel type="suppress">
<referencedClass name="I" />
</errorLevel>
</InvalidCatch>
</issueHandlers>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
interface I {}
class E extends Exception implements I {}
function foo() : void {
throw new E();
}
function handleThrow(I $e) : void {
echo "about to throw";
throw $e;
}
try {
foo();
} catch (I $e) {
handleThrow($e);
}'
);
$this->analyzeFile($file_path, new Context());
}
public function testValidThrowValidCatch(): void
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<issueHandlers>
<InvalidCatch>
<errorLevel type="suppress">
<referencedClass name="I" />
</errorLevel>
</InvalidCatch>
<InvalidThrow>
<errorLevel type="suppress">
<referencedClass name="I" />
</errorLevel>
</InvalidThrow>
</issueHandlers>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
interface I {}
class E extends Exception implements I {}
function foo() : void {
throw new E();
}
function handleThrow(I $e) : void {
echo "about to throw";
throw $e;
}
try {
foo();
} catch (I $e) {
handleThrow($e);
}'
);
$this->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
{
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
2019-03-06 00:08:41 +01:00
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
2019-03-06 00:08:41 +01:00
'<?xml version="1.0"?>
<psalm>
<globals>
<var name="glob1" type="string" />
<var name="glob2" type="array{str:string}" />
<var name="glob3" type="ns\Clazz" />
<var name="glob4" type="string|null" />
2019-03-06 00:14:07 +01:00
<var name="_GET" type="array{str:string}" />
2019-03-06 00:08:41 +01:00
</globals>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace {
ord($glob1);
ord($glob2["str"]);
$glob3->func();
2019-03-06 00:14:07 +01:00
ord($_GET["str"]);
2019-03-06 00:08:41 +01:00
assert($glob4 !== null);
ord($glob4);
function example1(): void {
global $glob1, $glob2, $glob3, $glob4;
ord($glob1);
ord($glob2["str"]);
$glob3->func();
ord($glob4);
2019-03-06 00:14:07 +01:00
ord($_GET["str"]);
2019-03-06 00:08:41 +01:00
}
$glob1 = 0;
error_reporting($glob1);
2019-03-06 00:14:07 +01:00
$_GET["str"] = 0;
error_reporting($_GET["str"]);
2019-03-06 00:08:41 +01:00
function example2(): void {
global $glob1, $glob2, $glob3;
error_reporting($glob1);
ord($glob2["str"]);
$glob3->func();
2019-03-06 00:14:07 +01:00
ord($_GET["str"]);
2019-03-06 00:08:41 +01:00
}
}
namespace ns {
ord($glob1);
ord($glob2["str"]);
$glob3->func();
2019-03-06 00:14:07 +01:00
ord($_GET["str"]);
2019-03-06 00:08:41 +01:00
class Clazz {
public function func(): void {}
}
function example3(): void {
global $glob1, $glob2, $glob3;
ord($glob1);
ord($glob2["str"]);
$glob3->func();
2019-03-06 00:14:07 +01:00
ord($_GET["str"]);
2019-03-06 00:08:41 +01:00
}
}
namespace ns2 {
/** @psalm-suppress InvalidGlobal */
global $glob1, $glob2, $glob3;
ord($glob1);
ord($glob2["str"]);
$glob3->func();
2019-03-11 14:54:41 +01:00
}
namespace {
2019-08-20 04:45:24 +02:00
ord($glob1 ?: "str");
2019-03-11 14:54:41 +01:00
ord($_GET["str"] ?? "str");
2019-03-14 01:15:29 +01:00
function example4(): void {
global $glob1;
2019-08-20 04:45:24 +02:00
ord($glob1 ?: "str");
2019-03-14 01:15:29 +01:00
ord($_GET["str"] ?? "str");
}
2019-03-06 00:08:41 +01:00
}'
);
$this->analyzeFile($file_path, new Context());
}
public function testIgnoreExceptions(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm checkForThrowsDocblock="true" checkForThrowsInGlobalScope="true">
<ignoreExceptions>
<class name="Exc1" />
<class name="Exc2" onlyGlobalScope="true" />
<classAndDescendants name="Exc3" />
<classAndDescendants name="Exc4" onlyGlobalScope="true" />
<classAndDescendants name="Exc5" />
</ignoreExceptions>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class Exc1 extends Exception {}
/** @throws Exc1 */
function throwsExc1(): void {}
class Exc2 extends Exception {}
/** @throws Exc2 */
function throwsExc2(): void {}
class Exc3 extends Exception {}
/** @throws Exc3 */
function throwsExc3(): void {}
class Exc4 extends Exception {}
/** @throws Exc4 */
function throwsExc4(): void {}
interface Exc5 {}
interface Exc6 extends Exc5 {}
/**
* @psalm-suppress InvalidThrow
* @throws Exc6
*/
function throwsExc6() : void {}
throwsExc1();
throwsExc2();
throwsExc3();
throwsExc4();
throwsExc6();
function example() : void {
throwsExc6();
throwsExc1();
throwsExc3();
throwsExc6();
}'
);
$this->analyzeFile($file_path, new Context());
}
2019-05-17 00:36:36 +02:00
public function testNotIgnoredException() : void
{
2021-12-03 20:29:06 +01:00
$this->expectException(CodeException::class);
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('MissingThrowsDocblock');
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
2019-05-10 00:58:30 +02:00
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm checkForThrowsDocblock="true" checkForThrowsInGlobalScope="true">
<ignoreExceptions>
<class name="Exc1" />
<class name="Exc2" onlyGlobalScope="true" />
<classAndDescendants name="Exc3" />
<classAndDescendants name="Exc4" onlyGlobalScope="true" />
</ignoreExceptions>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class Exc2 extends Exception {}
function example() : void {
throw new Exc2();
}'
);
$this->analyzeFile($file_path, new Context());
}
2019-06-05 06:00:42 +02:00
public function testGetPossiblePsr4Path(): void
2019-06-05 06:00:42 +02:00
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<directory name="tests" />
</projectFiles>
</psalm>'
)
);
$config = $this->project_analyzer->getConfig();
2021-12-03 20:11:20 +01:00
$classloader = new ClassLoader();
2019-06-05 06:00:42 +02:00
$classloader->addPsr4(
'Psalm\\',
[
dirname(__DIR__, 2)
. DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
2019-07-05 22:24:00 +02:00
. 'src' . DIRECTORY_SEPARATOR . 'Psalm',
2019-06-05 06:00:42 +02:00
]
);
$classloader->addPsr4(
'Psalm\\Tests\\',
[
dirname(__DIR__, 2)
. DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
2019-07-05 22:24:00 +02:00
. 'tests',
2019-06-05 06:00:42 +02:00
]
);
$config->setComposerClassLoader($classloader);
$this->assertSame(
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Psalm' . DIRECTORY_SEPARATOR . 'Foo.php',
2019-07-05 22:24:00 +02:00
$config->getPotentialComposerFilePathForClassLike('Psalm\\Foo')
2019-06-05 06:00:42 +02:00
);
$this->assertSame(
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'Foo.php',
2019-07-05 22:24:00 +02:00
$config->getPotentialComposerFilePathForClassLike('Psalm\\Tests\\Foo')
2019-06-05 06:00:42 +02:00
);
}
public function testTakesPhpVersionFromConfigFile(): void
{
$cfg = Config::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?><psalm phpVersion="7.1"></psalm>'
);
$this->assertSame('7.1', $cfg->getPhpVersion());
}
public function testReadsComposerJsonForPhpVersion(): void
{
$root = __DIR__ . '/../fixtures/ComposerPhpVersion';
$cfg = Config::loadFromXML($root, "<?xml version=\"1.0\"?><psalm></psalm>");
$this->assertSame('7.2', $cfg->getPhpVersion());
$cfg = Config::loadFromXML($root, "<?xml version=\"1.0\"?><psalm phpVersion='8.0'></psalm>");
$this->assertSame('8.0', $cfg->getPhpVersion());
}
2020-07-17 16:09:42 +02:00
public function testSetsUsePhpStormMetaPath(): void
2020-07-17 16:09:42 +02:00
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm usePhpStormMetaPath="false">
</psalm>'
)
);
$this->assertFalse($this->project_analyzer->getConfig()->use_phpstorm_meta_path);
}
public function testSetsUniversalObjectCrates(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<universalObjectCrates>
<class name="DateTime" />
</universalObjectCrates>
</psalm>'
)
);
$this->assertContains('datetime', $this->project_analyzer->getConfig()->getUniversalObjectCrates());
}
public function testInferPropertyTypesFromConstructorIsRead(): void
{
$cfg = Config::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?><psalm inferPropertyTypesFromConstructor="false"></psalm>'
);
$this->assertFalse($cfg->infer_property_types_from_constructor);
}
/**
* @return array<string, array{0: int, 1: int|null}>
*/
public function pluginRegistersScannerAndAnalyzerDataProvider(): array
{
return [
'regular' => [0, null], // flags, expected exception code
'invalid scanner class' => [FileTypeSelfRegisteringPlugin::FLAG_SCANNER_INVALID, 1622727271],
'invalid analyzer class' => [FileTypeSelfRegisteringPlugin::FLAG_ANALYZER_INVALID, 1622727281],
'override scanner' => [FileTypeSelfRegisteringPlugin::FLAG_SCANNER_TWICE, 1622727272],
'override analyzer' => [FileTypeSelfRegisteringPlugin::FLAG_ANALYZER_TWICE, 1622727282],
];
}
/**
* @test
* @dataProvider pluginRegistersScannerAndAnalyzerDataProvider
*/
public function pluginRegistersScannerAndAnalyzer(int $flags, ?int $expectedExceptionCode): void
{
$extension = uniqid('test');
$names = [
'scanner' => uniqid('PsalmTestFileTypeScanner'),
'analyzer' => uniqid('PsalmTestFileTypeAnaylzer'),
'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;
$projectAnalyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
sprintf(
'<?xml version="1.0"?>
<psalm><plugins><pluginClass class="%s"/></plugins></psalm>',
FileTypeSelfRegisteringPlugin::class
)
)
);
try {
$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');
}
2016-12-29 16:24:10 +01:00
}