1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-02 17:52:45 +01:00
psalm/tests/Config/ConfigTest.php

1312 lines
42 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
2019-07-05 22:24:00 +02:00
use function array_map;
use function define;
use function defined;
use const DIRECTORY_SEPARATOR;
use function dirname;
use function error_get_last;
use function getcwd;
use function implode;
use function is_array;
use function preg_match;
2016-12-29 16:24:10 +01:00
use Psalm\Config;
use Psalm\Context;
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
use Psalm\Internal\RuntimeCaches;
use Psalm\Tests\Internal\Provider;
2019-05-10 00:58:30 +02:00
use Psalm\Tests\TestConfig;
use function realpath;
use function symlink;
2019-07-05 22:24:00 +02:00
use function unlink;
2016-12-29 16:24:10 +01:00
2019-05-10 00:58:30 +02:00
class ConfigTest extends \Psalm\Tests\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', '2.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();
Refactor scanning and analysis, introducing multithreading (#191) * Add failing test * Add visitor to soup up classlike references * Move a whole bunch of code into the visitor * Move some methods back, move onto analysis stage * Use the getAliases method everywhere * Fix refs * Fix more refs * Fix some tests * Fix more tests * Fix include tests * Shift config class finding to project checker and fix bugs * Fix a few more tests * transition test to new syntax * Remove var_dump * Delete a bunch of code and fix mutation test * Remove unnecessary visitation * Transition to better mocked out file provider, breaking some cached statement loading * Use different scheme for naming anonymous classes * Fix anonymous class issues * Refactor file/statement loading * Add specific property types * Fix mapped property assignment * Improve how we deal with traits * Fix trait checking * Pass Psalm checks * Add multi-process support * Delay console output until the end * Remove PHP 7 syntax * Update file storage with classes * Fix scanning individual files and add reflection return types * Always turn XDebug off * Add quicker method of getting method mutations * Queue return types for crawling * Interpret all strings as possible classes once we see a `get_class` call * Check invalid return types again * Fix template namespacing issues * Default to class-insensitive file names for includes * Don’t overwrite existing issues data * Add var docblocks for scanning * Add null check * Fix loading of external classes in templates * Only try to populate class when we haven’t yet seen it’s not a class * Fix trait property accessibility * Only ever improve docblock param type * Make param replacement more robust * Fix static const missing inferred type * Fix a few more tests * Register constant definitions * Fix trait aliasing * Skip constant type tests for now * Fix linting issues * Make sure caching is off for tests * Remove unnecessary return * Use emulative parser if on PHP 5.6 * Cache parser for faster first-time parse * Fix constant resolution when scanning classes * Remove test that’s beyond a practical scope * Add back --diff support * Add --help for --threads * Remove unused vars
2017-07-25 22:11:02 +02:00
$this->file_provider = new Provider\FakeFileProvider();
2016-12-29 16:24:10 +01:00
}
private function getProjectAnalyzerWithConfig(Config $config): \Psalm\Internal\Analyzer\ProjectAnalyzer
{
2019-02-07 21:27:43 +01:00
$p = new \Psalm\Internal\Analyzer\ProjectAnalyzer(
$config,
2018-11-06 03:57:36 +01:00
new \Psalm\Internal\Provider\Providers(
$this->file_provider,
new Provider\FakeParserCacheProvider()
)
);
2019-02-07 21:27:43 +01:00
$p->setPhpVersion('7.3');
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')));
}
/**
* @return void
*/
public function testIgnoreSymlinkedProjectDirectory()
{
2019-05-10 00:58:30 +02:00
@unlink(dirname(__DIR__, 1) . '/fixtures/symlinktest/ignored/b');
$no_symlinking_error = 'symlink(): Cannot create symlink, error code(1314)';
$last_error = error_get_last();
$check_symlink_error =
2019-03-23 19:27:54 +01:00
!is_array($last_error) ||
!isset($last_error['message']) ||
$no_symlinking_error !== $last_error['message'];
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();
if (is_array($last_error) && $no_symlinking_error === $last_error['message']) {
$this->markTestSkipped($no_symlinking_error);
return;
}
}
$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'])) {
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(
'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";
},
2019-05-03 21:29:44 +02:00
\Psalm\Config\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(\Psalm\Exception\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 testAllowedEchoFunction(): 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></psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
echo "hello";'
);
$this->analyzeFile($file_path, new Context());
}
public function testForbiddenEchoFunctionViaFunctions(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('ForbiddenCode');
$this->expectException(\Psalm\Exception\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>
<forbiddenFunctions>
<function name="echo" />
</forbiddenFunctions>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
echo "hello";'
);
$this->analyzeFile($file_path, new Context());
}
public function testForbiddenEchoFunctionViaFlag(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('ForbiddenEcho');
$this->expectException(\Psalm\Exception\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 forbidEcho="true"></psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
echo "hello";'
);
$this->analyzeFile($file_path, new Context());
}
public function testAllowedPrintFunction(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm></psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
print "hello";'
);
$this->analyzeFile($file_path, new Context());
}
public function testForbiddenPrintFunction(): void
{
$this->expectExceptionMessage('ForbiddenCode');
$this->expectException(\Psalm\Exception\CodeException::class);
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<forbiddenFunctions>
<function name="print" />
</forbiddenFunctions>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
print "hello";'
);
$this->analyzeFile($file_path, new Context());
}
public function testAllowedVarExportFunction(): 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></psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
$a = [1, 2, 3];
var_export($a);'
);
$this->analyzeFile($file_path, new Context());
}
public function testForbiddenVarExportFunction(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('ForbiddenCode');
$this->expectException(\Psalm\Exception\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>
<forbiddenFunctions>
<function name="var_export" />
</forbiddenFunctions>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
$a = [1, 2, 3];
var_export($a);'
);
$this->analyzeFile($file_path, new Context());
}
public function testValidThrowInvalidCatch(): void
{
2019-05-17 00:36:36 +02:00
$this->expectExceptionMessage('InvalidCatch');
$this->expectException(\Psalm\Exception\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');
$this->expectException(\Psalm\Exception\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" />
</ignoreExceptions>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class Exc1 extends Exception {}
class Exc2 extends Exception {}
class Exc3 extends Exception {}
class Exc4 extends Exception {}
throw new Exc1();
throw new Exc2();
throw new Exc3();
throw new Exc4();
function example() : void {
throw new Exc1();
throw new Exc3();
}'
);
$this->analyzeFile($file_path, new Context());
}
2019-05-17 00:36:36 +02:00
public function testNotIgnoredException() : void
{
2019-05-17 00:36:36 +02:00
$this->expectException(\Psalm\Exception\CodeException::class);
$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();
$classloader = new \Composer\Autoload\ClassLoader();
$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);
}
/** @return void */
public function testSetsUniversalObjectCrates()
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<universalObjectCrates>
<class name="Foo" />
</universalObjectCrates>
</psalm>'
)
);
$this->assertContains('foo', $this->project_analyzer->getConfig()->getUniversalObjectCrates());
}
2016-12-29 16:24:10 +01:00
}