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