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);