> */ private static function getCodeBlocksFromDocs() { $issues_dir = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'docs' . DIRECTORY_SEPARATOR . 'running_psalm' . DIRECTORY_SEPARATOR . 'issues'; if (!file_exists($issues_dir)) { throw new \UnexpectedValueException('docs not found'); } $issue_code = []; foreach (glob($issues_dir . '/*.md') as $file_path) { $file_contents = file_get_contents($file_path); $file_lines = explode("\n", $file_contents); $current_issue = str_replace('# ', '', array_shift($file_lines)); for ($i = 0, $j = count($file_lines); $i < $j; ++$i) { $current_line = $file_lines[$i]; if (substr($current_line, 0, 6) === '```php' && $current_issue) { $current_block = ''; ++$i; do { $current_block .= $file_lines[$i] . "\n"; ++$i; } while (substr($file_lines[$i], 0, 3) !== '```' && $i < $j); $issue_code[$current_issue][] = trim($current_block); continue 2; } } } return $issue_code; } /** * @return void */ public function setUp() : void { FileAnalyzer::clearCache(); \Psalm\Internal\FileManipulation\FunctionDocblockManipulator::clearCache(); $this->file_provider = new Provider\FakeFileProvider(); $this->project_analyzer = new \Psalm\Internal\Analyzer\ProjectAnalyzer( new TestConfig(), new \Psalm\Internal\Provider\Providers( $this->file_provider, new Provider\FakeParserCacheProvider() ) ); $this->project_analyzer->setPhpVersion('7.3'); } /** * @return void */ public function testAllIssuesCovered() { $all_issues = \Psalm\Config\IssueHandler::getAllIssueTypes(); $all_issues[] = 'ParseError'; $all_issues[] = 'PluginIssue'; sort($all_issues); $code_blocks = self::getCodeBlocksFromDocs(); // these cannot have code $code_blocks['UnrecognizedExpression'] = true; $code_blocks['UnrecognizedStatement'] = true; $code_blocks['PluginIssue'] = true; $code_blocks['TaintedInput'] = true; // these are deprecated $code_blocks['TypeCoercion'] = true; $code_blocks['MixedTypeCoercion'] = true; $code_blocks['MixedTypeCoercion'] = true; $code_blocks['MisplacedRequiredParam'] = true; $documented_issues = array_keys($code_blocks); sort($documented_issues); $this->assertSame(implode("\n", $all_issues), implode("\n", $documented_issues)); } /** * @dataProvider providerInvalidCodeParse * @small * * @param string $code * @param string $error_message * @param array $error_levels * @param bool $check_references * * @return void */ public function testInvalidCode($code, $error_message, $error_levels = [], $check_references = false) { if (strpos($this->getTestName(), 'SKIPPED-') !== false) { $this->markTestSkipped(); } if ($check_references) { $this->project_analyzer->getCodebase()->reportUnusedCode(); $this->project_analyzer->trackUnusedSuppressions(); } $is_array_offset_test = strpos($error_message, 'ArrayOffset') && strpos($error_message, 'PossiblyUndefined') !== false; $this->project_analyzer->getConfig()->ensure_array_string_offsets_exist = $is_array_offset_test; $this->project_analyzer->getConfig()->ensure_array_int_offsets_exist = $is_array_offset_test; foreach ($error_levels as $error_level) { $this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS); } $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/'); $file_path = self::$src_dir_path . 'somefile.php'; $this->addFile($file_path, $code); $this->analyzeFile($file_path, new Context()); if ($check_references) { $this->project_analyzer->consolidateAnalyzedData(); } } /** * @return array */ public function providerInvalidCodeParse() { $invalid_code_data = []; foreach (self::getCodeBlocksFromDocs() as $issue_name => $blocks) { switch ($issue_name) { case 'MissingThrowsDocblock': continue 2; case 'UncaughtThrowInGlobalScope': continue 2; case 'InvalidStringClass': continue 2; case 'ForbiddenEcho': continue 2; case 'PluginClass': continue 2; case 'InvalidFalsableReturnType': $ignored_issues = ['FalsableReturnStatement']; break; case 'InvalidNullableReturnType': $ignored_issues = ['NullableReturnStatement']; break; case 'InvalidReturnType': $ignored_issues = ['InvalidReturnStatement']; break; case 'MixedInferredReturnType': $ignored_issues = ['MixedReturnStatement']; break; case 'MixedStringOffsetAssignment': $ignored_issues = ['MixedAssignment']; break; case 'ParadoxicalCondition': $ignored_issues = ['MissingParamType']; break; case 'UnusedClass': case 'UnusedMethod': $ignored_issues = ['UnusedVariable']; break; default: $ignored_issues = []; } $invalid_code_data[$issue_name] = [ $blocks[0], $issue_name, $ignored_issues, strpos($issue_name, 'Unused') !== false || strpos($issue_name, 'Unevaluated') !== false || strpos($issue_name, 'Unnecessary') !== false, ]; } return $invalid_code_data; } }