diff --git a/bin/phar.psalm.xml b/bin/phar.psalm.xml
index 01098a2d8..7fd4c4886 100644
--- a/bin/phar.psalm.xml
+++ b/bin/phar.psalm.xml
@@ -2,7 +2,7 @@
-
+
+
+
+ Setting `totallyTyped` to `"true"` is equivalent to setting `errorLevel` to `"1"`. Setting `totallyTyped` to `"false"` is equivalent to setting `errorLevel` to `"2"` and `reportMixedIssues` to `"false"`
+
+
+
+
+ Deprecated. Replaced by `errorLevel` and `reportMixedIssues`.
+
+
+
diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php
index 5c8237d38..3f67d8fb0 100644
--- a/dictionaries/CallMap.php
+++ b/dictionaries/CallMap.php
@@ -2535,7 +2535,7 @@ return [
'error_log' => ['bool', 'message'=>'string', 'message_type='=>'int', 'destination='=>'string', 'additional_headers='=>'string'],
'error_reporting' => ['int', 'error_level='=>'int'],
'ErrorException::__clone' => ['void'],
-'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'lineno='=>'int', 'previous='=>'?Throwable|?ErrorException'],
+'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'line='=>'int', 'previous='=>'?Throwable|?ErrorException'],
'ErrorException::__toString' => ['string'],
'ErrorException::getCode' => ['int'],
'ErrorException::getFile' => ['string'],
diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php
index 8ca16b883..206ea13ec 100644
--- a/dictionaries/CallMap_historical.php
+++ b/dictionaries/CallMap_historical.php
@@ -1382,7 +1382,7 @@ return [
'Error::getTrace' => ['list>'],
'Error::getTraceAsString' => ['string'],
'ErrorException::__clone' => ['void'],
- 'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'lineno='=>'int', 'previous='=>'?Throwable|?ErrorException'],
+ 'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'line='=>'int', 'previous='=>'?Throwable|?ErrorException'],
'ErrorException::__toString' => ['string'],
'ErrorException::getCode' => ['int'],
'ErrorException::getFile' => ['string'],
diff --git a/dictionaries/PropertyMap.php b/dictionaries/PropertyMap.php
index e0f91665d..e5d326244 100644
--- a/dictionaries/PropertyMap.php
+++ b/dictionaries/PropertyMap.php
@@ -256,7 +256,7 @@ return [
'client_info' => 'string',
'client_version' => 'int',
'connect_errno' => 'int',
- 'connect_error' => 'string',
+ 'connect_error' => '?string',
'errno' => 'int',
'error' => 'string',
'error_list' => 'array',
diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md
index b94c84805..f8735e390 100644
--- a/docs/running_psalm/configuration.md
+++ b/docs/running_psalm/configuration.md
@@ -416,6 +416,13 @@ Whether or not to allow `require`/`include` calls in your PHP. Defaults to `true
```
Allows you to hard-code a serializer for Psalm to use when caching data. By default, Psalm uses `ext-igbinary` *if* the version is greater than or equal to 2.0.5, otherwise it defaults to PHP's built-in serializer.
+#### threads
+```xml
+
+```
+Allows you to hard-code the number of threads Psalm will use (similar to `--threads` on the command line). This value will be used in place of detecting threads from the host machine, but will be overridden by using `--threads` or `--debug` (which sets threads to 1) on the command line
## Project settings
diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php
index 790de749a..ac4355b55 100644
--- a/src/Psalm/Config.php
+++ b/src/Psalm/Config.php
@@ -584,6 +584,9 @@ class Config
*/
public $internal_stubs = [];
+ /** @var ?int */
+ public $threads;
+
protected function __construct()
{
self::$instance = $this;
@@ -811,7 +814,9 @@ class Config
$deprecated_attributes = [
'allowCoercionFromStringToClassConst',
'allowPhpStormGenerics',
- 'forbidEcho'
+ 'forbidEcho',
+ 'loadXdebugStub',
+ 'totallyTyped'
];
$deprecated_elements = [
@@ -1241,6 +1246,10 @@ class Config
}
}
+ if (isset($config_xml->threads)) {
+ $config->threads = (int)$config_xml->threads;
+ }
+
return $config;
}
@@ -1781,11 +1790,22 @@ class Config
public function getReportingLevelForFunction(string $issue_type, string $function_id): ?string
{
+ $level = null;
if (isset($this->issue_handlers[$issue_type])) {
- return $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
+ $level = $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
+
+ if ($level === null && $issue_type === 'UndefinedFunction') {
+ // undefined functions trigger global namespace fallback
+ // so we should also check reporting levels for the symbol in global scope
+ $root_function_id = preg_replace('/.*\\\/', '', $function_id);
+ if ($root_function_id !== $function_id) {
+ /** @psalm-suppress PossiblyUndefinedStringArrayOffset https://github.com/vimeo/psalm/issues/7656 */
+ $level = $this->issue_handlers[$issue_type]->getReportingLevelForFunction($root_function_id);
+ }
+ }
}
- return null;
+ return $level;
}
public function getReportingLevelForArgument(string $issue_type, string $function_id): ?string
@@ -2009,6 +2029,12 @@ class Config
$this->internal_stubs[] = $ext_decimal_path;
}
+ // phpredis
+ if (extension_loaded('redis')) {
+ $ext_phpredis_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'phpredis.phpstub';
+ $this->internal_stubs[] = $ext_phpredis_path;
+ }
+
foreach ($this->internal_stubs as $stub_path) {
if (!file_exists($stub_path)) {
throw new UnexpectedValueException('Cannot locate ' . $stub_path);
diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php
index 3b9470d6f..73e642af9 100644
--- a/src/Psalm/Context.php
+++ b/src/Psalm/Context.php
@@ -108,6 +108,13 @@ class Context
*/
public $inside_assignment = false;
+ /**
+ * Whether or not we're inside a try block.
+ *
+ * @var bool
+ */
+ public $inside_try = false;
+
/**
* @var null|CodeLocation
*/
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php
index 8ecffb85d..5074bd68a 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php
@@ -86,9 +86,12 @@ class TryAnalyzer
$old_referenced_var_ids = $try_context->referenced_var_ids;
+ $was_inside_try = $context->inside_try;
+ $context->inside_try = true;
if ($statements_analyzer->analyze($stmt->stmts, $context) === false) {
return false;
}
+ $context->inside_try = $was_inside_try;
if ($try_context->finally_scope) {
foreach ($context->vars_in_scope as $var_id => $type) {
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
index 407c6d044..50ccf068d 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
@@ -315,6 +315,14 @@ class AssignmentAnalyzer
$assign_value_type->parent_nodes = [
$assignment_node->id => $assignment_node
];
+
+ if ($context->inside_try) {
+ // Copy previous assignment's parent nodes inside a try. Since an exception could be thrown at any
+ // point this is a workaround to ensure that use of a variable also uses all previous assignments.
+ if (isset($context->vars_in_scope[$array_var_id])) {
+ $assign_value_type->parent_nodes += $context->vars_in_scope[$array_var_id]->parent_nodes;
+ }
+ }
}
if ($array_var_id && isset($context->vars_in_scope[$array_var_id])) {
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php
index 4432087d9..481a5afa0 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php
@@ -16,6 +16,8 @@ use Psalm\Type\Atomic\TCallableString;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Union;
+use function dirname;
+
class MagicConstAnalyzer
{
public static function analyze(
@@ -84,10 +86,16 @@ class MagicConstAnalyzer
} else {
$statements_analyzer->node_data->setType($stmt, new Union([new TCallableString]));
}
- } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File
- || $stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir
- ) {
- $statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()]));
+ } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir) {
+ $statements_analyzer->node_data->setType(
+ $stmt,
+ Type::getString(dirname($statements_analyzer->getSource()->getFilePath()))
+ );
+ } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File) {
+ $statements_analyzer->node_data->setType(
+ $stmt,
+ Type::getString($statements_analyzer->getSource()->getFilePath())
+ );
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Trait_) {
if ($statements_analyzer->getSource() instanceof TraitAnalyzer) {
$statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()]));
diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php
index 36043f4cf..0f41c9974 100644
--- a/src/Psalm/Internal/Cli/Psalm.php
+++ b/src/Psalm/Internal/Cli/Psalm.php
@@ -246,7 +246,7 @@ final class Psalm
$options['long-progress'] = true;
}
- $threads = self::detectThreads($options, $in_ci);
+ $threads = self::detectThreads($options, $config, $in_ci);
self::emitMacPcreWarning($options, $threads);
@@ -909,12 +909,14 @@ final class Psalm
}
}
- private static function detectThreads(array $options, bool $in_ci): int
+ private static function detectThreads(array $options, Config $config, bool $in_ci): int
{
if (isset($options['threads'])) {
$threads = (int)$options['threads'];
} elseif (isset($options['debug']) || $in_ci) {
$threads = 1;
+ } elseif ($config->threads) {
+ $threads = $config->threads;
} else {
$threads = max(1, ProjectAnalyzer::getCpuCount() - 1);
}
diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php
index 9de676135..5c666574f 100644
--- a/src/Psalm/IssueBuffer.php
+++ b/src/Psalm/IssueBuffer.php
@@ -691,7 +691,7 @@ class IssueBuffer
: $error_count . ' errors'
) . ' found' . "\n";
} else {
- self::printSuccessMessage();
+ self::printSuccessMessage($project_analyzer);
}
$show_info = $project_analyzer->stdout_report_options->show_info;
@@ -782,8 +782,12 @@ class IssueBuffer
}
}
- public static function printSuccessMessage(): void
+ public static function printSuccessMessage(ProjectAnalyzer $project_analyzer): void
{
+ if (!$project_analyzer->stdout_report_options) {
+ throw new UnexpectedValueException('Cannot print success message without stdout report options');
+ }
+
// this message will be printed
$message = "No errors found!";
@@ -808,9 +812,15 @@ class IssueBuffer
// text style, 1 = bold
$style = "1";
- echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n";
- echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\e[0m" . "\n";
- echo "\e[{$background};{$style}m{$paddingBottom}\e[0m" . "\n";
+ if ($project_analyzer->stdout_report_options->use_color) {
+ echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n";
+ echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\e[0m" . "\n";
+ echo "\e[{$background};{$style}m{$paddingBottom}\e[0m" . "\n";
+ } else {
+ echo "\n";
+ echo "$messageWithPadding\n";
+ echo "\n";
+ }
}
/**
diff --git a/stubs/Php81.phpstub b/stubs/Php81.phpstub
index 97dd281e1..d3398388d 100644
--- a/stubs/Php81.phpstub
+++ b/stubs/Php81.phpstub
@@ -56,6 +56,13 @@ namespace {
*/
public function getBackingValue(): int|string;
}
+
+ class ReflectionIntersectionType extends ReflectionType {
+ /**
+ * @return non-empty-list
+ */
+ public function getTypes() {}
+ }
}
namespace FTP {
diff --git a/stubs/phpredis.phpstub b/stubs/phpredis.phpstub
new file mode 100644
index 000000000..65dd5f7c2
--- /dev/null
+++ b/stubs/phpredis.phpstub
@@ -0,0 +1,542 @@
+
-
+
@@ -185,9 +183,7 @@ class ConfigFileTest extends TestCase
$abcEnabled = trim('
-
+
diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php
index 6a15f41c9..867099ff7 100644
--- a/tests/Config/ConfigTest.php
+++ b/tests/Config/ConfigTest.php
@@ -355,6 +355,36 @@ class ConfigTest extends TestCase
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php')));
}
+ 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(
diff --git a/tests/IssueBufferTest.php b/tests/IssueBufferTest.php
index a1f871477..7da31f16a 100644
--- a/tests/IssueBufferTest.php
+++ b/tests/IssueBufferTest.php
@@ -113,8 +113,10 @@ class IssueBufferTest extends TestCase
public function testPrintSuccessMessageWorks(): void
{
+ $project_analyzer = $this->createMock(ProjectAnalyzer::class);
+ $project_analyzer->stdout_report_options = new ReportOptions;
ob_start();
- IssueBuffer::printSuccessMessage();
+ IssueBuffer::printSuccessMessage($project_analyzer);
$output = ob_get_clean();
$this->assertStringContainsString('No errors found!', $output);
diff --git a/tests/TaintTest.php b/tests/TaintTest.php
index 75419f8a6..302ca2434 100644
--- a/tests/TaintTest.php
+++ b/tests/TaintTest.php
@@ -2285,6 +2285,25 @@ class TaintTest extends TestCase
echo sinkNotWorking($_GET["taint"]);',
'error_message' => 'TaintedHtml',
],
+ 'taintEscapedInTryMightNotWork' => [
+ ' 'TaintedHtml',
+ ],
];
}
diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php
index 61fdb840f..048025186 100644
--- a/tests/UnusedVariableTest.php
+++ b/tests/UnusedVariableTest.php
@@ -2436,6 +2436,43 @@ class UnusedVariableTest extends TestCase
$a = false;
}'
],
+ 'usedInCatchIsAlwaysUsedInTry' => [
+ ' [
+ ' [
+ ' 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array, parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41'
],
+ 'warnAboutUnusedVariableInTryReassignedInCatch' => [
+ ' 'UnusedVariable',
+ ],
+ 'warnAboutUnusedVariableInTryReassignedInFinally' => [
+ ' 'UnusedVariable',
+ ],
+ 'SKIPPED-warnAboutVariableUsedInNestedTryNotUsedInOuterTry' => [
+ ' 'UnusedVariable',
+ ],
];
}
}
diff --git a/tests/fixtures/DestructiveAutoloader/psalm.xml b/tests/fixtures/DestructiveAutoloader/psalm.xml
index ec68f8f2e..3a4cc64a8 100644
--- a/tests/fixtures/DestructiveAutoloader/psalm.xml
+++ b/tests/fixtures/DestructiveAutoloader/psalm.xml
@@ -1,6 +1,5 @@