mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Daniil Gentili
1986c8b4a8
* Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389) * Default to sealed arrays * Fix array_merge bug * Fixes * Fix * Sealed type checks * Properly infer properties-of and get_object_vars on final classes * Fix array_map zipping * Fix tests * Fixes * Fixes * Fix more stuff * Recursively resolve type aliases * Fix typo * Fixes * Fix array_is_list assertion on keyed array * Add BC docs * Fixes * fix * Update * Update * Update * Update * Seal arrays with count assertions * Fix #8528 * Fix * Update * Improve sealed array foreach logic * get_object_vars on template properties * Fix sealed array assertion reconciler logic * Improved reconciler * Add tests * Single source of truth for test types * Fix tests * Fixup tests * Fixup tests * Fixup tests * Update * Fix tests * Fix tests * Final fixes * Fixes * Use list syntax only when needed * Fix tests * Cs-fix * Update docs * Update docs * Update docs * Update docs * Update docs * Document missing types * Update docs * Improve class-string-map docs * Update * Update * I love working on psalm :) * Keep arrays unsealed by default * Fixup tests * Fix syntax mistake * cs-fix * Fix typo * Re-import missing types * Keep strict types only in return types * argc/argv fixes * argc/argv fixes * Fix test * Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
282 lines
9.3 KiB
PHP
282 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace Psalm\Tests\EndToEnd;
|
|
|
|
use Exception;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
use function closedir;
|
|
use function copy;
|
|
use function file_exists;
|
|
use function file_get_contents;
|
|
use function file_put_contents;
|
|
use function getcwd;
|
|
use function is_dir;
|
|
use function is_string;
|
|
use function mkdir;
|
|
use function opendir;
|
|
use function preg_replace;
|
|
use function readdir;
|
|
use function rmdir;
|
|
use function str_replace;
|
|
use function substr_count;
|
|
use function sys_get_temp_dir;
|
|
use function tempnam;
|
|
use function unlink;
|
|
|
|
/**
|
|
* Tests some of the most important use cases of the psalm and psalter commands, by launching a new
|
|
* process as if invoked by a real user.
|
|
*
|
|
* This is primarily intended to test the code in `psalm`, `src/psalm.php` and related files.
|
|
*/
|
|
class PsalmEndToEndTest extends TestCase
|
|
{
|
|
use PsalmRunnerTrait;
|
|
|
|
/** @var string */
|
|
private static $tmpDir;
|
|
|
|
public static function setUpBeforeClass(): void
|
|
{
|
|
self::$tmpDir = tempnam(sys_get_temp_dir(), 'PsalmEndToEndTest_');
|
|
unlink(self::$tmpDir);
|
|
mkdir(self::$tmpDir);
|
|
|
|
$getcwd = getcwd();
|
|
if (!is_string($getcwd)) {
|
|
throw new Exception('Couldn\'t get working directory');
|
|
}
|
|
|
|
mkdir(self::$tmpDir . '/src');
|
|
|
|
copy(__DIR__ . '/../fixtures/DummyProjectWithErrors/composer.json', self::$tmpDir . '/composer.json');
|
|
|
|
$process = new Process(['composer', 'install', '--no-plugins'], self::$tmpDir, null, null, 120);
|
|
$process->mustRun();
|
|
}
|
|
|
|
public static function tearDownAfterClass(): void
|
|
{
|
|
self::recursiveRemoveDirectory(self::$tmpDir);
|
|
parent::tearDownAfterClass();
|
|
}
|
|
|
|
public function setUp(): void
|
|
{
|
|
@unlink(self::$tmpDir . '/psalm.xml');
|
|
copy(
|
|
__DIR__ . '/../fixtures/DummyProjectWithErrors/src/FileWithErrors.php',
|
|
self::$tmpDir . '/src/FileWithErrors.php'
|
|
);
|
|
parent::setUp();
|
|
}
|
|
|
|
public function tearDown(): void
|
|
{
|
|
if (file_exists(self::$tmpDir . '/cache')) {
|
|
self::recursiveRemoveDirectory(self::$tmpDir . '/cache');
|
|
}
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testHelpReturnsMessage(): void
|
|
{
|
|
$this->assertStringContainsString('Usage:', $this->runPsalm(['--help'], self::$tmpDir)['STDOUT']);
|
|
}
|
|
|
|
public function testInit(): void
|
|
{
|
|
$this->assertStringStartsWith(
|
|
'Calculating best config level based on project files',
|
|
$this->runPsalmInit()['STDOUT']
|
|
);
|
|
$this->assertFileExists(self::$tmpDir . '/psalm.xml');
|
|
}
|
|
|
|
public function testAlter(): void
|
|
{
|
|
$this->runPsalmInit();
|
|
|
|
$this->assertStringContainsString(
|
|
'No errors found!',
|
|
$this->runPsalm(['--alter', '--issues=all'], self::$tmpDir, false, true)['STDOUT']
|
|
);
|
|
|
|
$this->assertSame(0, $this->runPsalm([], self::$tmpDir)['CODE']);
|
|
}
|
|
|
|
public function testPsalter(): void
|
|
{
|
|
$this->runPsalmInit();
|
|
(new Process(['php', $this->psalter, '--alter', '--issues=InvalidReturnType'], self::$tmpDir))->mustRun();
|
|
$this->assertSame(0, $this->runPsalm([], self::$tmpDir)['CODE']);
|
|
}
|
|
|
|
public function testPsalm(): void
|
|
{
|
|
$this->runPsalmInit(1);
|
|
$result = $this->runPsalm([], self::$tmpDir, true);
|
|
$this->assertStringContainsString(
|
|
'Target PHP version: 7.1 (inferred from composer.json)',
|
|
$result['STDERR']
|
|
);
|
|
$this->assertStringContainsString('UnusedParam', $result['STDOUT']);
|
|
$this->assertStringContainsString('InvalidReturnType', $result['STDOUT']);
|
|
$this->assertStringContainsString('InvalidReturnStatement', $result['STDOUT']);
|
|
$this->assertStringContainsString('3 errors', $result['STDOUT']);
|
|
$this->assertSame(2, $result['CODE']);
|
|
}
|
|
|
|
public function testPsalmWithPHPVersionOverride(): void
|
|
{
|
|
$this->runPsalmInit(1);
|
|
$result = $this->runPsalm(['--php-version=8.0'], self::$tmpDir, true);
|
|
$this->assertStringContainsString(
|
|
'Target PHP version: 8.0 (set by CLI argument)',
|
|
$result['STDERR']
|
|
);
|
|
}
|
|
|
|
public function testPsalmWithPHPVersionFromConfig(): void
|
|
{
|
|
$this->runPsalmInit(1, '7.4');
|
|
$result = $this->runPsalm([], self::$tmpDir, true);
|
|
$this->assertStringContainsString(
|
|
'Target PHP version: 7.4 (set by config file)',
|
|
$result['STDERR']
|
|
);
|
|
}
|
|
|
|
public function testPsalmDiff(): void
|
|
{
|
|
copy(__DIR__ . '/../fixtures/DummyProjectWithErrors/diff_composer.lock', self::$tmpDir . '/composer.lock');
|
|
|
|
$this->runPsalmInit(1);
|
|
$result = $this->runPsalm(['--diff', '-m'], self::$tmpDir, true);
|
|
$this->assertStringContainsString('UnusedParam', $result['STDOUT']);
|
|
$this->assertStringContainsString('InvalidReturnType', $result['STDOUT']);
|
|
$this->assertStringContainsString('InvalidReturnStatement', $result['STDOUT']);
|
|
$this->assertStringContainsString('3 errors', $result['STDOUT']);
|
|
$this->assertStringContainsString('E', $result['STDERR']);
|
|
|
|
$this->assertSame(2, $result['CODE']);
|
|
|
|
$result = $this->runPsalm(['--diff', '-m'], self::$tmpDir, true);
|
|
|
|
$this->assertStringContainsString('UnusedParam', $result['STDOUT']);
|
|
$this->assertStringContainsString('InvalidReturnType', $result['STDOUT']);
|
|
$this->assertStringContainsString('InvalidReturnStatement', $result['STDOUT']);
|
|
$this->assertStringContainsString('3 errors', $result['STDOUT']);
|
|
$this->assertEquals(1, substr_count($result['STDERR'], 'E')); // Should only have 'E' from 'Extensions' in version message
|
|
|
|
$this->assertSame(2, $result['CODE']);
|
|
|
|
@unlink(self::$tmpDir . '/composer.lock');
|
|
}
|
|
|
|
public function testTainting(): void
|
|
{
|
|
$this->runPsalmInit(1);
|
|
$result = $this->runPsalm(['--taint-analysis'], self::$tmpDir, true);
|
|
|
|
$this->assertStringContainsString('TaintedHtml', $result['STDOUT']);
|
|
$this->assertStringContainsString('TaintedTextWithQuotes', $result['STDOUT']);
|
|
$this->assertStringContainsString('2 errors', $result['STDOUT']);
|
|
$this->assertSame(2, $result['CODE']);
|
|
}
|
|
|
|
public function testTaintingWithoutInit(): void
|
|
{
|
|
$result = $this->runPsalm(['--taint-analysis'], self::$tmpDir, true, false);
|
|
|
|
$this->assertStringContainsString('TaintedHtml', $result['STDOUT']);
|
|
$this->assertStringContainsString('TaintedTextWithQuotes', $result['STDOUT']);
|
|
$this->assertStringContainsString('2 errors', $result['STDOUT']);
|
|
$this->assertSame(2, $result['CODE']);
|
|
}
|
|
|
|
public function testTaintGraphDumping(): void
|
|
{
|
|
$this->runPsalmInit(1);
|
|
$result = $this->runPsalm(
|
|
[
|
|
'--taint-analysis',
|
|
'--dump-taint-graph='.self::$tmpDir.'/taints.dot',
|
|
],
|
|
self::$tmpDir,
|
|
true
|
|
);
|
|
|
|
$this->assertSame(2, $result['CODE']);
|
|
$this->assertFileEquals(
|
|
__DIR__ . '/../fixtures/expected_taint_graph.dot',
|
|
self::$tmpDir.'/taints.dot'
|
|
);
|
|
}
|
|
|
|
public function testLegacyConfigWithoutresolveFromConfigFile(): void
|
|
{
|
|
$this->runPsalmInit(1);
|
|
$psalmXmlContent = file_get_contents(self::$tmpDir . '/psalm.xml');
|
|
$count = 0;
|
|
$psalmXmlContent = preg_replace('/resolveFromConfigFile="true"/', 'resolveFromConfigFile="false"', $psalmXmlContent, -1, $count);
|
|
$this->assertEquals(1, $count);
|
|
|
|
file_put_contents(self::$tmpDir . '/src/psalm.xml', $psalmXmlContent);
|
|
|
|
$process = new Process(['php', $this->psalm, '--config=src/psalm.xml'], self::$tmpDir);
|
|
$process->run();
|
|
$this->assertSame(2, $process->getExitCode());
|
|
$this->assertStringContainsString('InvalidReturnType', $process->getOutput());
|
|
}
|
|
|
|
/**
|
|
* @return strict-array{STDOUT: string, STDERR: string, CODE: int|null}
|
|
*/
|
|
private function runPsalmInit(?int $level = null, ?string $php_version = null): array
|
|
{
|
|
$args = ['--init'];
|
|
|
|
if ($level) {
|
|
$args[] = 'src';
|
|
$args[] = (string) $level;
|
|
}
|
|
|
|
$ret = $this->runPsalm($args, self::$tmpDir, false, false);
|
|
|
|
$psalm_config_contents = file_get_contents(self::$tmpDir . '/psalm.xml');
|
|
$psalm_config_contents = str_replace(
|
|
'errorLevel="1"',
|
|
'errorLevel="1" '
|
|
. 'cacheDirectory="' . self::$tmpDir . '/cache" '
|
|
. ($php_version ? ('phpVersion="' . $php_version . '"') : ''),
|
|
$psalm_config_contents
|
|
);
|
|
file_put_contents(self::$tmpDir . '/psalm.xml', $psalm_config_contents);
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/** from comment by itay at itgoldman dot com at
|
|
* https://www.php.net/manual/en/function.rmdir.php#117354
|
|
*/
|
|
private static function recursiveRemoveDirectory(string $src): void
|
|
{
|
|
$dir = opendir($src);
|
|
while (false !== ($file = readdir($dir))) {
|
|
if (($file !== '.') && ($file !== '..')) {
|
|
$full = $src . '/' . $file;
|
|
if (is_dir($full)) {
|
|
self::recursiveRemoveDirectory($full);
|
|
} else {
|
|
unlink($full);
|
|
}
|
|
}
|
|
}
|
|
closedir($dir);
|
|
rmdir($src);
|
|
}
|
|
}
|