diff --git a/config.xsd b/config.xsd index a6f72f097..c77f4fb2f 100644 --- a/config.xsd +++ b/config.xsd @@ -96,6 +96,11 @@ + + + + + @@ -144,7 +149,7 @@ - + diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 42cfad3b5..f77cfc889 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -258,6 +258,11 @@ class Config */ private $mock_classes = []; + /** + * @var array + */ + private $preloaded_stub_files = []; + /** * @var array */ @@ -1031,7 +1036,17 @@ class Config ); } - $config->addStubFile($file_path); + if (isset($stub_file['preloadClasses'])) { + $preload_classes = (string)$stub_file['preloadClasses']; + + if ($preload_classes === 'true' || $preload_classes === '1') { + $config->addPreloadedStubFile($file_path); + } else { + $config->addStubFile($file_path); + } + } else { + $config->addStubFile($file_path); + } } } @@ -1700,6 +1715,56 @@ class Config return $this->mock_classes; } + public function visitPreloadedStubFiles(Codebase $codebase, ?Progress $progress = null): void + { + if ($progress === null) { + $progress = new VoidProgress(); + } + + $core_generic_files = []; + + if (\PHP_VERSION_ID < 80000 && $codebase->php_major_version >= 8) { + $stringable_path = dirname(__DIR__, 2) . '/stubs/Stringable.php'; + + if (!file_exists($stringable_path)) { + throw new \UnexpectedValueException('Cannot locate core generic classes'); + } + + $core_generic_files[] = $stringable_path; + } + + $stub_files = array_merge($core_generic_files, $this->preloaded_stub_files); + + if ($this->load_xdebug_stub) { + $xdebug_stub_path = dirname(__DIR__, 2) . '/stubs/Xdebug.php'; + + if (!file_exists($xdebug_stub_path)) { + throw new \UnexpectedValueException('Cannot locate XDebug stub'); + } + + $stub_files[] = $xdebug_stub_path; + } + + if (!$stub_files) { + return; + } + + foreach ($stub_files as $file_path) { + $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path); + $codebase->scanner->addFileToDeepScan($file_path); + } + + $progress->debug('Registering preloaded stub files' . "\n"); + + $codebase->register_stub_files = true; + + $codebase->scanFiles(); + + $codebase->register_stub_files = false; + + $progress->debug('Finished registering preloaded stub files' . "\n"); + } + public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): void { if ($progress === null) { @@ -1741,16 +1806,6 @@ class Config $core_generic_files[] = $ext_ds_path; } - if (\version_compare(\PHP_VERSION, '8.0', '<') && $codebase->php_major_version >= 8) { - $stringable_path = dirname(__DIR__, 2) . '/stubs/Stringable.php'; - - if (!file_exists($stringable_path)) { - throw new \UnexpectedValueException('Cannot locate core generic classes'); - } - - $core_generic_files[] = $stringable_path; - } - $stub_files = array_merge($core_generic_files, $this->stub_files); $phpstorm_meta_path = $this->base_dir . DIRECTORY_SEPARATOR . '.phpstorm.meta.php'; @@ -1769,16 +1824,6 @@ class Config } } - if ($this->load_xdebug_stub) { - $xdebug_stub_path = dirname(__DIR__, 2) . '/stubs/Xdebug.php'; - - if (!file_exists($xdebug_stub_path)) { - throw new \UnexpectedValueException('Cannot locate XDebug stub'); - } - - $stub_files[] = $xdebug_stub_path; - } - foreach ($stub_files as $file_path) { $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path); $codebase->scanner->addFileToDeepScan($file_path); @@ -2016,6 +2061,11 @@ class Config return $this->stub_files; } + public function addPreloadedStubFile(string $stub_file): void + { + $this->preloaded_stub_files[$stub_file] = $stub_file; + } + public function getPhpVersion(): ?string { if (isset($this->configured_php_version)) { diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index c2703ee5d..32477cc8e 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -582,6 +582,8 @@ class ProjectAnalyzer $this->config->initializePlugins($this); + $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); + $this->codebase->scanFiles($this->threads); $this->codebase->infer_types_from_usage = true; @@ -604,6 +606,8 @@ class ProjectAnalyzer $this->config->initializePlugins($this); + $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); + $this->codebase->scanFiles($this->threads); } else { $diff_no_files = true; @@ -987,6 +991,8 @@ class ProjectAnalyzer $this->config->initializePlugins($this); + $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); + $this->codebase->scanFiles($this->threads); $this->config->visitStubFiles($this->codebase, $this->progress); @@ -1120,6 +1126,8 @@ class ProjectAnalyzer $this->config->initializePlugins($this); + $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); + $this->codebase->scanFiles($this->threads); $this->config->visitStubFiles($this->codebase, $this->progress); @@ -1158,6 +1166,8 @@ class ProjectAnalyzer $this->config->initializePlugins($this); + $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); + $this->codebase->scanFiles($this->threads); $this->config->visitStubFiles($this->codebase, $this->progress); diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 6f34336ce..2af2e0410 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -245,7 +245,9 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements FileSour $classlike_storage->class_implements['stringable'] = 'Stringable'; } - $this->codebase->scanner->queueClassLikeForScanning('Stringable'); + if (\PHP_VERSION_ID >= 80000) { + $this->codebase->scanner->queueClassLikeForScanning('Stringable'); + } } if (!$this->scan_deep) { diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index 0a109ac9f..6f9b9b433 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -171,6 +171,9 @@ class DocumentationTest extends TestCase $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/'); + $codebase = $this->project_analyzer->getCodebase(); + $codebase->config->visitPreloadedStubFiles($codebase); + $file_path = self::$src_dir_path . 'somefile.php'; $this->addFile($file_path, $code); diff --git a/tests/TestCase.php b/tests/TestCase.php index a6ffe5205..2b9a12379 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -94,6 +94,7 @@ class TestCase extends BaseTestCase public function analyzeFile($file_path, \Psalm\Context $context, bool $track_unused_suppressions = true): void { $codebase = $this->project_analyzer->getCodebase(); + $codebase->addFilesToAnalyze([$file_path => $file_path]); $codebase->scanFiles(); diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index a088b9690..3d838b0de 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -391,6 +391,9 @@ class ToStringTest extends TestCase ], 'implicitStringableDisallowed' => [ 'expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/'); } + $codebase = $this->project_analyzer->getCodebase(); + $codebase->config->visitPreloadedStubFiles($codebase); + $this->addFile($file_path, $code); $this->analyzeFile($file_path, new Context()); } diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index 50bc91483..855672fe2 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -76,6 +76,9 @@ trait ValidCodeAnalysisTestTrait $this->project_analyzer->setPhpVersion($php_version); + $codebase = $this->project_analyzer->getCodebase(); + $codebase->config->visitPreloadedStubFiles($codebase); + $file_path = self::$src_dir_path . 'somefile.php'; $this->addFile($file_path, $code);