file_provider = new Provider\FakeFileProvider(); } private function getProjectAnalyzerWithConfig(Config $config): \Psalm\Internal\Analyzer\ProjectAnalyzer { $p = new \Psalm\Internal\Analyzer\ProjectAnalyzer( $config, new \Psalm\Internal\Provider\Providers( $this->file_provider, new Provider\FakeParserCacheProvider() ) ); $p->setPhpVersion('7.3'); 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/StringAnalyzer.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/StringAnalyzer.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('does/not/exist/FileAnalyzer.php'))); $this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php'))); } /** * @return void */ public function testIgnoreSymlinkedProjectDirectory() { @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 = !is_array($last_error) || !isset($last_error['message']) || $no_symlinking_error !== $last_error['message']; @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( 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/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 { $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/StringAnalyzer.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/StringAnalyzer.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'))); $this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.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('tests/ConfigTest.php'))); $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php'))); } 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' ) ); } public function testAllPossibleIssues(): void { $all_possible_handlers = implode( ' ', array_map( /** * @param string $issue_name * * @return string */ function ($issue_name): string { return '<' . $issue_name . ' errorLevel="suppress" />' . "\n"; }, \Psalm\Config\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(\Psalm\Exception\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 testExitFunctions(): 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 testAllowedEchoFunction(): 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 testForbiddenEchoFunctionViaFunctions(): void { $this->expectExceptionMessage('ForbiddenCode'); $this->expectException(\Psalm\Exception\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 testForbiddenEchoFunctionViaFlag(): void { $this->expectExceptionMessage('ForbiddenEcho'); $this->expectException(\Psalm\Exception\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 testAllowedPrintFunction(): 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 testForbiddenPrintFunction(): void { $this->expectExceptionMessage('ForbiddenCode'); $this->expectException(\Psalm\Exception\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 testAllowedVarExportFunction(): 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 testForbiddenVarExportFunction(): void { $this->expectExceptionMessage('ForbiddenCode'); $this->expectException(\Psalm\Exception\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 testValidThrowInvalidCatch(): void { $this->expectExceptionMessage('InvalidCatch'); $this->expectException(\Psalm\Exception\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(\Psalm\Exception\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 { 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"]); } $glob1 = 0; error_reporting($glob1); $_GET["str"] = 0; error_reporting($_GET["str"]); function example2(): void { global $glob1, $glob2, $glob3; error_reporting($glob1); 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(\Psalm\Exception\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 \Composer\Autoload\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); } /** @return void */ public function testSetsUniversalObjectCrates() { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2), ' ' ) ); $this->assertContains('datetime', $this->project_analyzer->getConfig()->getUniversalObjectCrates()); } }