diff --git a/UPGRADING.md b/UPGRADING.md
index 2bf88c584a4..9b3665d4dcf 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -12,8 +12,11 @@
- [BC] The only optional boolean parameter of `TKeyedArray::getGenericArrayType` was removed, and was replaced with a string parameter with a different meaning.
- [BC] The `TDependentListKey` type was removed and replaced with an optional property of the `TIntRange` type.
+-
- [BC] `TCallableArray` and `TCallableList` removed and replaced with `TCallableKeyedArray`.
+- [BC] Class `Psalm\Issue\MixedInferredReturnType` was removed
+
- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well.
- [BC] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint`
diff --git a/config.xsd b/config.xsd
index 72745ea604f..6a6d182dca3 100644
--- a/config.xsd
+++ b/config.xsd
@@ -47,6 +47,7 @@
+
@@ -333,7 +334,6 @@
-
@@ -494,6 +494,7 @@
+
diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md
index 346b525f878..ba14e7d67c9 100644
--- a/docs/annotating_code/supported_annotations.md
+++ b/docs/annotating_code/supported_annotations.md
@@ -202,7 +202,7 @@ takesFoo(getFoo());
This provides the same, but for `false`. Psalm uses this internally for functions like `preg_replace`, which can return false if the given input has encoding errors, but where 99.9% of the time the function operates as expected.
-### `@psalm-seal-properties`, `@psalm-no-seal-properties`
+### `@psalm-seal-properties`, `@psalm-no-seal-properties`, `@seal-properties`, `@no-seal-properties`
If you have a magic property getter/setter, you can use `@psalm-seal-properties` to instruct Psalm to disallow getting and setting any properties not contained in a list of `@property` (or `@property-read`/`@property-write`) annotations.
This is automatically enabled with the configuration option `sealAllProperties` and can be disabled for a class with `@psalm-no-seal-properties`
@@ -211,7 +211,7 @@ This is automatically enabled with the configuration option `sealAllProperties`
bar = 5; // this call fails
```
-### `@psalm-seal-methods`, `@psalm-no-seal-methods`
+### `@psalm-seal-methods`, `@psalm-no-seal-methods`, `@seal-methods`, `@no-seal-methods`
If you have a magic method caller, you can use `@psalm-seal-methods` to instruct Psalm to disallow calling any methods not contained in a list of `@method` annotations.
This is automatically enabled with the configuration option `sealAllMethods` and can be disabled for a class with `@psalm-no-seal-methods`
@@ -236,7 +236,7 @@ This is automatically enabled with the configuration option `sealAllMethods` and
+
+
+
+```
diff --git a/psalm.xml.dist b/psalm.xml.dist
index 1452823757e..5e8e8ac33d0 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -13,6 +13,7 @@
errorBaseline="psalm-baseline.xml"
findUnusedPsalmSuppress="true"
findUnusedBaselineEntry="true"
+ findUnusedIssueHandlerSuppression="true"
>
@@ -63,24 +64,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -104,12 +87,6 @@
-
-
-
-
-
-
diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php
index 8759c0ddbcd..dfb3360693e 100644
--- a/src/Psalm/Config.php
+++ b/src/Psalm/Config.php
@@ -156,7 +156,6 @@ final class Config
'MixedArrayTypeCoercion',
'MixedAssignment',
'MixedFunctionCall',
- 'MixedInferredReturnType',
'MixedMethodCall',
'MixedOperand',
'MixedPropertyFetch',
@@ -230,6 +229,8 @@ final class Config
*/
public string $base_dir;
+ public ?string $source_filename = null;
+
/**
* The PHP version to assume as declared in the config file
*/
@@ -369,6 +370,8 @@ final class Config
public bool $find_unused_baseline_entry = true;
+ public bool $find_unused_issue_handler_suppression = true;
+
public bool $run_taint_analysis = false;
public bool $use_phpstorm_meta_path = true;
@@ -935,6 +938,7 @@ private static function fromXmlAndPaths(
'allowNamedArgumentCalls' => 'allow_named_arg_calls',
'findUnusedPsalmSuppress' => 'find_unused_psalm_suppress',
'findUnusedBaselineEntry' => 'find_unused_baseline_entry',
+ 'findUnusedIssueHandlerSuppression' => 'find_unused_issue_handler_suppression',
'reportInfo' => 'report_info',
'restrictReturnTypes' => 'restrict_return_types',
'limitMethodComplexity' => 'limit_method_complexity',
@@ -950,6 +954,7 @@ private static function fromXmlAndPaths(
}
}
+ $config->source_filename = $config_path;
if ($config->resolve_from_config_file) {
$config->base_dir = $base_dir;
} else {
@@ -1311,6 +1316,12 @@ public function setComposerClassLoader(?ClassLoader $loader = null): void
$this->composer_class_loader = $loader;
}
+ /** @return array */
+ public function getIssueHandlers(): array
+ {
+ return $this->issue_handlers;
+ }
+
public function setAdvancedErrorLevel(string $issue_key, array $config, ?string $default_error_level = null): void
{
$this->issue_handlers[$issue_key] = new IssueHandler();
@@ -1858,6 +1869,30 @@ public static function getParentIssueType(string $issue_type): ?string
return null;
}
+ /** @return array{type: string, index: int, count: int}[] */
+ public function getIssueHandlerSuppressions(): array
+ {
+ $suppressions = [];
+ foreach ($this->issue_handlers as $key => $handler) {
+ foreach ($handler->getFilters() as $index => $filter) {
+ $suppressions[] = [
+ 'type' => $key,
+ 'index' => $index,
+ 'count' => $filter->suppressions,
+ ];
+ }
+ }
+ return $suppressions;
+ }
+
+ /** @param array{type: string, index: int, count: int}[] $filters */
+ public function combineIssueHandlerSuppressions(array $filters): void
+ {
+ foreach ($filters as $filter) {
+ $this->issue_handlers[$filter['type']]->getFilters()[$filter['index']]->suppressions += $filter['count'];
+ }
+ }
+
public function getReportingLevelForFile(string $issue_type, string $file_path): string
{
if (isset($this->issue_handlers[$issue_type])) {
diff --git a/src/Psalm/Config/ErrorLevelFileFilter.php b/src/Psalm/Config/ErrorLevelFileFilter.php
index 1778ccfae35..de3ed732c19 100644
--- a/src/Psalm/Config/ErrorLevelFileFilter.php
+++ b/src/Psalm/Config/ErrorLevelFileFilter.php
@@ -15,6 +15,8 @@ final class ErrorLevelFileFilter extends FileFilter
{
private string $error_level = '';
+ public int $suppressions = 0;
+
public static function loadFromArray(
array $config,
string $base_dir,
diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php
index a5af5aefe4b..aba87f0232b 100644
--- a/src/Psalm/Config/IssueHandler.php
+++ b/src/Psalm/Config/IssueHandler.php
@@ -25,7 +25,7 @@ final class IssueHandler
private string $error_level = Config::REPORT_ERROR;
/**
- * @var array
+ * @var list
*/
private array $custom_levels = [];
@@ -50,6 +50,12 @@ public static function loadFromXMLElement(SimpleXMLElement $e, string $base_dir)
return $handler;
}
+ /** @return list */
+ public function getFilters(): array
+ {
+ return $this->custom_levels;
+ }
+
public function setCustomLevels(array $customLevels, string $base_dir): void
{
/** @var array $customLevel */
@@ -71,6 +77,7 @@ public function getReportingLevelForFile(string $file_path): string
{
foreach ($this->custom_levels as $custom_level) {
if ($custom_level->allows($file_path)) {
+ $custom_level->suppressions++;
return $custom_level->getErrorLevel();
}
}
@@ -82,6 +89,7 @@ public function getReportingLevelForClass(string $fq_classlike_name): ?string
{
foreach ($this->custom_levels as $custom_level) {
if ($custom_level->allowsClass($fq_classlike_name)) {
+ $custom_level->suppressions++;
return $custom_level->getErrorLevel();
}
}
@@ -93,6 +101,7 @@ public function getReportingLevelForMethod(string $method_id): ?string
{
foreach ($this->custom_levels as $custom_level) {
if ($custom_level->allowsMethod(strtolower($method_id))) {
+ $custom_level->suppressions++;
return $custom_level->getErrorLevel();
}
}
@@ -115,6 +124,7 @@ public function getReportingLevelForArgument(string $function_id): ?string
{
foreach ($this->custom_levels as $custom_level) {
if ($custom_level->allowsMethod(strtolower($function_id))) {
+ $custom_level->suppressions++;
return $custom_level->getErrorLevel();
}
}
@@ -126,6 +136,7 @@ public function getReportingLevelForProperty(string $property_id): ?string
{
foreach ($this->custom_levels as $custom_level) {
if ($custom_level->allowsProperty($property_id)) {
+ $custom_level->suppressions++;
return $custom_level->getErrorLevel();
}
}
@@ -137,6 +148,7 @@ public function getReportingLevelForClassConstant(string $constant_id): ?string
{
foreach ($this->custom_levels as $custom_level) {
if ($custom_level->allowsClassConstant($constant_id)) {
+ $custom_level->suppressions++;
return $custom_level->getErrorLevel();
}
}
@@ -148,6 +160,7 @@ public function getReportingLevelForVariable(string $var_name): ?string
{
foreach ($this->custom_levels as $custom_level) {
if ($custom_level->allowsVariable($var_name)) {
+ $custom_level->suppressions++;
return $custom_level->getErrorLevel();
}
}
diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
index f6d03381690..ae4b2480902 100644
--- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
@@ -258,8 +258,6 @@ public function analyze(
IssueBuffer::maybeAdd($docblock_issue);
}
- $classlike_storage_provider = $codebase->classlike_storage_provider;
-
$parent_fq_class_name = $this->parent_fq_class_name;
if ($class instanceof PhpParser\Node\Stmt\Class_ && $class->extends && $parent_fq_class_name) {
@@ -626,43 +624,7 @@ public function analyze(
}
$pseudo_methods = $storage->pseudo_methods + $storage->pseudo_static_methods;
-
- foreach ($pseudo_methods as $pseudo_method_name => $pseudo_method_storage) {
- $pseudo_method_id = new MethodIdentifier(
- $this->fq_class_name,
- $pseudo_method_name,
- );
-
- $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id);
-
- if ($overridden_method_ids
- && $pseudo_method_name !== '__construct'
- && $pseudo_method_storage->location
- ) {
- foreach ($overridden_method_ids as $overridden_method_id) {
- $parent_method_storage = $codebase->methods->getStorage($overridden_method_id);
-
- $overridden_fq_class_name = $overridden_method_id->fq_class_name;
-
- $parent_storage = $classlike_storage_provider->get($overridden_fq_class_name);
-
- MethodComparator::compare(
- $codebase,
- null,
- $storage,
- $parent_storage,
- $pseudo_method_storage,
- $parent_method_storage,
- $this->fq_class_name,
- $pseudo_method_storage->visibility ?: 0,
- $storage->location ?: $pseudo_method_storage->location,
- $storage->suppressed_issues,
- true,
- false,
- );
- }
- }
- }
+ MethodComparator::comparePseudoMethods($pseudo_methods, $this->fq_class_name, $codebase, $storage);
$event = new AfterClassLikeAnalysisEvent(
$class,
diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
index 8f0264e7caf..572df0f5078 100644
--- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
@@ -261,9 +261,10 @@ private static function decorateVarDocblockComment(
public static function sanitizeDocblockType(string $docblock_type): string
{
$docblock_type = (string) preg_replace('@^[ \t]*\*@m', '', $docblock_type);
- $docblock_type = (string) preg_replace('/,\n\s+}/', '}', $docblock_type);
+ $docblock_type = (string) preg_replace('/,[\n\s]+}/', '}', $docblock_type);
+ $docblock_type = (string) preg_replace('/[ \t]+/', ' ', $docblock_type);
- return str_replace("\n", '', $docblock_type);
+ return trim(str_replace("\n", '', $docblock_type));
}
/**
@@ -431,6 +432,10 @@ public static function getVarComments(
$var_comments = [];
try {
+ $file_path = $statements_analyzer->getRootFilePath();
+ $file_storage_provider = $codebase->file_storage_provider;
+ $file_storage = $file_storage_provider->get($file_path);
+
$var_comments = $codebase->config->disable_var_parsing
? []
: self::arrayToDocblocks(
@@ -439,6 +444,7 @@ public static function getVarComments(
$statements_analyzer->getSource(),
$statements_analyzer->getSource()->getAliases(),
$statements_analyzer->getSource()->getTemplateTypeMap(),
+ $file_storage->type_aliases,
);
} catch (IncorrectDocblockException $e) {
IssueBuffer::maybeAdd(
diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
index 9f8d8864525..65d38b25096 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
@@ -40,7 +40,6 @@
use Psalm\Issue\MismatchingDocblockReturnType;
use Psalm\Issue\MissingClosureReturnType;
use Psalm\Issue\MissingReturnType;
-use Psalm\Issue\MixedInferredReturnType;
use Psalm\Issue\MixedReturnTypeCoercion;
use Psalm\Issue\MoreSpecificReturnType;
use Psalm\Issue\UnresolvableConstant;
@@ -516,17 +515,6 @@ public static function verifyReturnType(
}
if ($inferred_return_type->hasMixed()) {
- if (IssueBuffer::accepts(
- new MixedInferredReturnType(
- 'Could not verify return type \'' . $declared_return_type . '\' for ' .
- $cased_method_id,
- $return_type_location,
- ),
- $suppressed_issues,
- )) {
- return false;
- }
-
return null;
}
diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
index 2a2ec630c34..5e3f4151fb7 100644
--- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
@@ -217,6 +217,10 @@ public function analyze(): void
}
}
+ $pseudo_methods = $class_storage->pseudo_methods + $class_storage->pseudo_static_methods;
+
+ MethodComparator::comparePseudoMethods($pseudo_methods, $this->fq_class_name, $codebase, $class_storage);
+
$statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider());
$statements_analyzer->analyze($member_stmts, $interface_context, null, true);
diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php
index a5fb8becc13..cef877dc3c7 100644
--- a/src/Psalm/Internal/Analyzer/MethodComparator.php
+++ b/src/Psalm/Internal/Analyzer/MethodComparator.php
@@ -23,6 +23,8 @@
use Psalm\Issue\LessSpecificImplementedReturnType;
use Psalm\Issue\MethodSignatureMismatch;
use Psalm\Issue\MethodSignatureMustProvideReturnType;
+use Psalm\Issue\MismatchingDocblockParamType;
+use Psalm\Issue\MismatchingDocblockReturnType;
use Psalm\Issue\MissingImmutableAnnotation;
use Psalm\Issue\MoreSpecificImplementedParamType;
use Psalm\Issue\OverriddenMethodAccess;
@@ -238,6 +240,56 @@ public static function compare(
return null;
}
+ /**
+ * @param array $pseudo_methods
+ */
+ public static function comparePseudoMethods(
+ array $pseudo_methods,
+ string $fq_class_name,
+ Codebase $codebase,
+ ClassLikeStorage $class_storage,
+ ): void {
+ foreach ($pseudo_methods as $pseudo_method_name => $pseudo_method_storage) {
+ $pseudo_method_id = new MethodIdentifier(
+ $fq_class_name,
+ $pseudo_method_name,
+ );
+
+ $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id);
+ if (isset($class_storage->methods[$pseudo_method_id->method_name])) {
+ $overridden_method_ids[$class_storage->name] = $pseudo_method_id;
+ }
+
+ if ($overridden_method_ids
+ && $pseudo_method_name !== '__construct'
+ && $pseudo_method_storage->location
+ ) {
+ foreach ($overridden_method_ids as $overridden_method_id) {
+ $parent_method_storage = $codebase->methods->getStorage($overridden_method_id);
+
+ $overridden_fq_class_name = $overridden_method_id->fq_class_name;
+
+ $parent_storage = $codebase->classlike_storage_provider->get($overridden_fq_class_name);
+
+ self::compare(
+ $codebase,
+ null,
+ $class_storage,
+ $parent_storage,
+ $pseudo_method_storage,
+ $parent_method_storage,
+ $fq_class_name,
+ $pseudo_method_storage->visibility ?: 0,
+ $class_storage->location ?: $pseudo_method_storage->location,
+ $class_storage->suppressed_issues,
+ true,
+ false,
+ );
+ }
+ }
+ }
+ }
+
/**
* @param string[] $suppressed_issues
*/
@@ -495,6 +547,7 @@ private static function compareMethodParams(
if ($guide_classlike_storage->user_defined
&& $implementer_param->signature_type
+ && $guide_param->signature_type
) {
self::compareMethodSignatureParams(
$codebase,
@@ -823,6 +876,18 @@ private static function compareMethodDocblockParams(
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues,
);
+ } elseif ($guide_class_name == $implementer_called_class_name) {
+ IssueBuffer::maybeAdd(
+ new MismatchingDocblockParamType(
+ 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id
+ . ' has wrong type \'' .
+ $implementer_method_storage_param_type->getId() . '\' in @method annotation, expecting \'' .
+ $guide_method_storage_param_type->getId() . '\'',
+ $implementer_method_storage->params[$i]->location
+ ?: $code_location,
+ ),
+ $suppressed_issues + $implementer_classlike_storage->suppressed_issues,
+ );
} else {
IssueBuffer::maybeAdd(
new ImplementedParamTypeMismatch(
@@ -1044,6 +1109,17 @@ private static function compareMethodDocblockReturnTypes(
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues,
);
+ } elseif ($guide_class_name == $implementer_called_class_name) {
+ IssueBuffer::maybeAdd(
+ new MismatchingDocblockReturnType(
+ 'The inherited return type \'' . $guide_method_storage_return_type->getId()
+ . '\' for ' . $cased_guide_method_id . ' is different to the corresponding '
+ . '@method annotation \'' . $implementer_method_storage_return_type->getId() . '\'',
+ $implementer_method_storage->return_type_location
+ ?: $code_location,
+ ),
+ $suppressed_issues + $implementer_classlike_storage->suppressed_issues,
+ );
} else {
IssueBuffer::maybeAdd(
new ImplementedReturnTypeMismatch(
diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
index fe12ae02391..2be20c66fe2 100644
--- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
@@ -1027,6 +1027,9 @@ public function checkFile(string $file_path): void
*/
public function checkPaths(array $paths_to_check): void
{
+ $this->progress->write($this->generatePHPVersionMessage());
+ $this->progress->startScanningFiles();
+
$this->config->visitPreloadedStubFiles($this->codebase, $this->progress);
$this->visitAutoloadFiles();
@@ -1046,9 +1049,6 @@ public function checkPaths(array $paths_to_check): void
$this->file_reference_provider->loadReferenceCache();
- $this->progress->write($this->generatePHPVersionMessage());
- $this->progress->startScanningFiles();
-
$this->config->initializePlugins($this);
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
index 288c3e16cfd..04e1c9e2e48 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
@@ -24,6 +24,8 @@
use Psalm\Issue\PossiblyNullOperand;
use Psalm\Issue\StringIncrement;
use Psalm\IssueBuffer;
+use Psalm\Node\Expr\BinaryOp\VirtualMinus;
+use Psalm\Node\Expr\BinaryOp\VirtualPlus;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Atomic;
@@ -820,6 +822,28 @@ private static function analyzeOperands(
$result_type = Type::getInt();
}
}
+ } elseif ($parent instanceof VirtualPlus || $parent instanceof VirtualMinus) {
+ $sum = $parent instanceof VirtualPlus ? 1 : -1;
+ if ($context && $context->inside_loop && $left_type_part instanceof TLiteralInt) {
+ if ($parent instanceof VirtualPlus) {
+ $new_type = new TIntRange($left_type_part->value + $sum, null);
+ } else {
+ $new_type = new TIntRange(null, $left_type_part->value + $sum);
+ }
+ } elseif ($left_type_part instanceof TLiteralInt) {
+ $new_type = new TLiteralInt($left_type_part->value + $sum);
+ } elseif ($left_type_part instanceof TIntRange) {
+ $start = $left_type_part->min_bound === null ? null : $left_type_part->min_bound + $sum;
+ $end = $left_type_part->max_bound === null ? null : $left_type_part->max_bound + $sum;
+ $new_type = new TIntRange($start, $end);
+ } else {
+ $new_type = new TInt();
+ }
+
+ $result_type = Type::combineUnionTypes(
+ new Union([$new_type], ['from_calculation' => true]),
+ $result_type,
+ );
} else {
$result_type = Type::combineUnionTypes(
$always_positive ? Type::getIntRange(1, null) : Type::getInt(true),
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php
index 6241127b579..14026d3d1ab 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php
@@ -435,6 +435,12 @@ private static function findPseudoMethodAndClassStorages(
}
$ancestors = $static_class_storage->class_implements;
+ foreach ($static_class_storage->namedMixins as $namedObject) {
+ $type = $namedObject->value;
+ if ($type) {
+ $ancestors[$type] = true;
+ }
+ }
foreach ($ancestors as $fq_class_name => $_) {
$class_storage = $codebase->classlikes->getStorageFor($fq_class_name);
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
index f53816e5814..0e081a2c67c 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
@@ -312,7 +312,6 @@ public static function checkMethodArgs(
$declaring_method_id = $class_storage->declaring_method_ids[$method_name];
$declaring_fq_class_name = $declaring_method_id->fq_class_name;
- $declaring_method_name = $declaring_method_id->method_name;
if ($declaring_fq_class_name !== $fq_class_name) {
$declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_fq_class_name);
@@ -320,11 +319,7 @@ public static function checkMethodArgs(
$declaring_class_storage = $class_storage;
}
- if (!isset($declaring_class_storage->methods[$declaring_method_name])) {
- throw new UnexpectedValueException('Storage should not be empty here');
- }
-
- $method_storage = $declaring_class_storage->methods[$declaring_method_name];
+ $method_storage = $codebase->methods->getStorage($declaring_method_id);
if ($declaring_class_storage->user_defined
&& !$method_storage->has_docblock_param_types
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php
index 659a7fdb42b..4b3fcc1a2d2 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php
@@ -12,6 +12,7 @@
use Psalm\FileSource;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\ArithmeticOpAnalyzer;
+use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\Type\TypeCombiner;
@@ -261,23 +262,28 @@ public static function infer(
}
if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) {
- $name = strtolower($stmt->name->getFirst());
- if ($name === 'false') {
+ $name = $stmt->name->getFirst();
+ $name_lowercase = strtolower($name);
+ if ($name_lowercase === 'false') {
return Type::getFalse();
}
- if ($name === 'true') {
+ if ($name_lowercase === 'true') {
return Type::getTrue();
}
- if ($name === 'null') {
+ if ($name_lowercase === 'null') {
return Type::getNull();
}
- if ($stmt->name->getFirst() === '__NAMESPACE__') {
+ if ($name === '__NAMESPACE__') {
return Type::getString($aliases->namespace);
}
+ if ($type = ConstFetchAnalyzer::getGlobalConstType($codebase, $name, $name)) {
+ return $type;
+ }
+
return null;
}
diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
index e47bb93978e..0ccadac662e 100644
--- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
@@ -677,7 +677,7 @@ private static function analyzeStatement(
$check_type_string,
$statements_analyzer->getAliases(),
);
- $check_type = Type::parseString($fq_check_type_string);
+ $check_type = Type::parseString(CommentAnalyzer::sanitizeDocblockType($fq_check_type_string));
/** @psalm-suppress InaccessibleProperty We just created this type */
$check_type->possibly_undefined = $possibly_undefined;
diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php
index a725158a6a9..80cdba04ddc 100644
--- a/src/Psalm/Internal/Codebase/Analyzer.php
+++ b/src/Psalm/Internal/Codebase/Analyzer.php
@@ -90,6 +90,7 @@
* used_suppressions: array>,
* function_docblock_manipulators: array>,
* mutable_classes: array,
+ * issue_handlers: array{type: string, index: int, count: int}[],
* }
*/
@@ -407,6 +408,10 @@ static function (): void {
IssueBuffer::addUsedSuppressions($pool_data['used_suppressions']);
}
+ if ($codebase->config->find_unused_issue_handler_suppression) {
+ $codebase->config->combineIssueHandlerSuppressions($pool_data['issue_handlers']);
+ }
+
if ($codebase->taint_flow_graph && $pool_data['taint_data']) {
$codebase->taint_flow_graph->addGraph($pool_data['taint_data']);
}
@@ -1628,6 +1633,7 @@ private function getWorkerData(): array
'used_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUsedSuppressions() : [],
'function_docblock_manipulators' => FunctionDocblockManipulator::getManipulators(),
'mutable_classes' => $codebase->analyzer->mutable_classes,
+ 'issue_handlers' => $this->config->getIssueHandlerSuppressions()
];
// @codingStandardsIgnoreEnd
}
diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php
index 8fcf208cef3..c93735ab0ae 100644
--- a/src/Psalm/Internal/Codebase/Methods.php
+++ b/src/Psalm/Internal/Codebase/Methods.php
@@ -450,6 +450,13 @@ public function getMethodParams(
foreach ($params as $i => $param) {
if (isset($overridden_storage->params[$i]->type)
&& $overridden_storage->params[$i]->has_docblock_type
+ && (
+ ! $param->type
+ || $param->type->equals(
+ $overridden_storage->params[$i]->signature_type
+ ?? $overridden_storage->params[$i]->type,
+ )
+ )
) {
$params[$i] = clone $param;
/** @var Union $params[$i]->type */
@@ -1132,14 +1139,18 @@ public function getStorage(MethodIdentifier $method_id): MethodStorage
}
$method_name = $method_id->method_name;
+ $method_storage = $class_storage->methods[$method_name]
+ ?? $class_storage->pseudo_methods[$method_name]
+ ?? $class_storage->pseudo_static_methods[$method_name]
+ ?? null;
- if (!isset($class_storage->methods[$method_name])) {
+ if (! $method_storage) {
throw new UnexpectedValueException(
'$storage should not be null for ' . $method_id,
);
}
- return $class_storage->methods[$method_name];
+ return $method_storage;
}
/** @psalm-mutation-free */
diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php
index c0753196f49..ccd3e2b65c5 100644
--- a/src/Psalm/Internal/Codebase/Populator.php
+++ b/src/Psalm/Internal/Codebase/Populator.php
@@ -352,7 +352,9 @@ private function populateOverriddenMethods(
$declaring_method_name = $declaring_method_id->method_name;
$declaring_class_storage = $declaring_class_storages[$declaring_class];
- $declaring_method_storage = $declaring_class_storage->methods[$declaring_method_name];
+ $declaring_method_storage = $declaring_class_storage->methods[$declaring_method_name]
+ ?? $declaring_class_storage->pseudo_methods[$declaring_method_name]
+ ?? $declaring_class_storage->pseudo_static_methods[$declaring_method_name];
if (($declaring_method_storage->has_docblock_param_types
|| $declaring_method_storage->has_docblock_return_type)
@@ -541,8 +543,16 @@ private function populateDataFromParentClass(
$parent_storage->dependent_classlikes[strtolower($storage->name)] = true;
- $storage->pseudo_methods += $parent_storage->pseudo_methods;
- $storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids;
+ foreach ($parent_storage->pseudo_methods as $method_name => $pseudo_method) {
+ if (!isset($storage->methods[$method_name])) {
+ $storage->pseudo_methods[$method_name] = $pseudo_method;
+ }
+ }
+ foreach ($parent_storage->declaring_pseudo_method_ids as $method_name => $pseudo_method_id) {
+ if (!isset($storage->methods[$method_name])) {
+ $storage->declaring_pseudo_method_ids[$method_name] = $pseudo_method_id;
+ };
+ }
}
private function populateInterfaceData(
diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php
index 17740d9f9a7..eabb6673e90 100644
--- a/src/Psalm/Internal/Codebase/Scanner.php
+++ b/src/Psalm/Internal/Codebase/Scanner.php
@@ -296,6 +296,7 @@ private function scanFilePaths(int $pool_size): bool
$pool_size = 1;
}
+ $this->progress->expand(count($files_to_scan));
if ($pool_size > 1) {
$process_file_paths = [];
@@ -334,7 +335,6 @@ function (): void {
*/
function () {
$this->progress->debug('Collecting data from forked scanner process' . PHP_EOL);
-
$project_analyzer = ProjectAnalyzer::getInstance();
$codebase = $project_analyzer->getCodebase();
$statements_provider = $codebase->statements_provider;
@@ -356,6 +356,9 @@ function () {
'taint_data' => $codebase->taint_flow_graph,
];
},
+ function (): void {
+ $this->progress->taskDone(0);
+ },
);
// Wait for all tasks to complete and collect the results.
@@ -406,6 +409,7 @@ function () {
$i = 0;
foreach ($files_to_scan as $file_path => $_) {
+ $this->progress->taskDone(0);
$this->scanAPath($i, $file_path);
++$i;
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php
index e94a64ba10f..be0ccb6102f 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php
@@ -69,9 +69,9 @@ public static function parse(
$templates = [];
if (isset($parsed_docblock->combined_tags['template'])) {
foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) {
- $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line));
+ $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line));
if ($template_type === false) {
- throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg());
+ throw new IncorrectDocblockException('Invalid @template tag: '.preg_last_error_msg());
}
$template_name = array_shift($template_type);
@@ -112,7 +112,7 @@ public static function parse(
if (isset($parsed_docblock->combined_tags['template-covariant'])) {
foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) {
- $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line));
+ $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line));
if ($template_type === false) {
throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg());
}
@@ -172,20 +172,16 @@ public static function parse(
if (isset($parsed_docblock->tags['psalm-require-extends'])
&& count($extension_requirements = $parsed_docblock->tags['psalm-require-extends']) > 0) {
- $info->extension_requirement = trim((string) preg_replace(
- '@^[ \t]*\*@m',
- '',
+ $info->extension_requirement = CommentAnalyzer::sanitizeDocblockType(
$extension_requirements[array_key_first($extension_requirements)],
- ));
+ );
}
if (isset($parsed_docblock->tags['psalm-require-implements'])) {
foreach ($parsed_docblock->tags['psalm-require-implements'] as $implementation_requirement) {
- $info->implementation_requirements[] = trim((string) preg_replace(
- '@^[ \t]*\*@m',
- '',
+ $info->implementation_requirements[] = CommentAnalyzer::sanitizeDocblockType(
$implementation_requirement,
- ));
+ );
}
}
@@ -200,7 +196,7 @@ public static function parse(
if (isset($parsed_docblock->tags['psalm-yield'])) {
$yield = (string) reset($parsed_docblock->tags['psalm-yield']);
- $info->yield = trim((string) preg_replace('@^[ \t]*\*@m', '', $yield));
+ $info->yield = CommentAnalyzer::sanitizeDocblockType($yield);
}
if (isset($parsed_docblock->tags['deprecated'])) {
@@ -241,18 +237,20 @@ public static function parse(
}
}
- if (isset($parsed_docblock->tags['psalm-seal-properties'])) {
- $info->sealed_properties = true;
- }
- if (isset($parsed_docblock->tags['psalm-no-seal-properties'])) {
- $info->sealed_properties = false;
- }
+ foreach (['', 'psalm-'] as $prefix) {
+ if (isset($parsed_docblock->tags[$prefix . 'seal-properties'])) {
+ $info->sealed_properties = true;
+ }
+ if (isset($parsed_docblock->tags[$prefix . 'no-seal-properties'])) {
+ $info->sealed_properties = false;
+ }
- if (isset($parsed_docblock->tags['psalm-seal-methods'])) {
- $info->sealed_methods = true;
- }
- if (isset($parsed_docblock->tags['psalm-no-seal-methods'])) {
- $info->sealed_methods = false;
+ if (isset($parsed_docblock->tags[$prefix . 'seal-methods'])) {
+ $info->sealed_methods = true;
+ }
+ if (isset($parsed_docblock->tags[$prefix . 'no-seal-methods'])) {
+ $info->sealed_methods = false;
+ }
}
if (isset($parsed_docblock->tags['psalm-inheritors'])) {
@@ -553,7 +551,7 @@ private static function addMagicPropertyToInfo(
$end = $offset + strlen($line_parts[0]);
- $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]));
+ $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);
if ($line_parts[0] === ''
|| ($line_parts[0][0] === '$'
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
index 4086fd16f57..89c7f3cd761 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
@@ -79,7 +79,6 @@
use function count;
use function implode;
use function preg_match;
-use function preg_replace;
use function preg_split;
use function str_replace;
use function strtolower;
@@ -608,11 +607,16 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
$storage->pseudo_static_methods[$lc_method_name] = $pseudo_method_storage;
} else {
$storage->pseudo_methods[$lc_method_name] = $pseudo_method_storage;
- $storage->declaring_pseudo_method_ids[$lc_method_name] = new MethodIdentifier(
- $fq_classlike_name,
- $lc_method_name,
- );
}
+ $method_identifier = new MethodIdentifier(
+ $fq_classlike_name,
+ $lc_method_name,
+ );
+ $storage->inheritable_method_ids[$lc_method_name] = $method_identifier;
+ if (!isset($storage->overridden_method_ids[$lc_method_name])) {
+ $storage->overridden_method_ids[$lc_method_name] = [];
+ }
+ $storage->declaring_pseudo_method_ids[$lc_method_name] = $method_identifier;
}
@@ -924,7 +928,7 @@ public function handleTraitUse(PhpParser\Node\Stmt\TraitUse $node): void
$this->useTemplatedType(
$storage,
$node,
- trim((string) preg_replace('@^[ \t]*\*@m', '', $template_line)),
+ CommentAnalyzer::sanitizeDocblockType($template_line),
);
}
}
@@ -1896,10 +1900,7 @@ private static function getTypeAliasesFromCommentLines(
continue;
}
- $var_line = (string) preg_replace('/[ \t]+/', ' ', (string) preg_replace('@^[ \t]*\*@m', '', $var_line));
- $var_line = (string) preg_replace('/,\n\s+\}/', '}', $var_line);
- $var_line = str_replace("\n", '', $var_line);
-
+ $var_line = CommentAnalyzer::sanitizeDocblockType($var_line);
$var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if (!$var_line_parts) {
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php
index 8d60023a4fc..03a5d8c8d0f 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php
@@ -153,11 +153,7 @@ public static function parse(
$line_parts[1] = substr($line_parts[1], 1);
}
- $line_parts[0] = str_replace(
- "\n",
- '',
- (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]),
- );
+ $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);
if ($line_parts[0] === ''
|| ($line_parts[0][0] === '$'
@@ -196,14 +192,10 @@ public static function parse(
$line_parts = CommentAnalyzer::splitDocLine($param);
if (count($line_parts) > 0) {
- $line_parts[0] = str_replace(
- "\n",
- '',
- (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]),
- );
+ $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);
$info->self_out = [
- 'type' => str_replace("\n", '', $line_parts[0]),
+ 'type' => $line_parts[0],
'line_number' => $comment->getStartLine() + substr_count(
$comment_text,
"\n",
@@ -227,10 +219,10 @@ public static function parse(
foreach ($parsed_docblock->tags['psalm-if-this-is'] as $offset => $param) {
$line_parts = CommentAnalyzer::splitDocLine($param);
- $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]));
+ $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);
$info->if_this_is = [
- 'type' => str_replace("\n", '', $line_parts[0]),
+ 'type' => $line_parts[0],
'line_number' => $comment->getStartLine() + substr_count(
$comment->getText(),
"\n",
@@ -456,7 +448,7 @@ public static function parse(
$templates = [];
if (isset($parsed_docblock->combined_tags['template'])) {
foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) {
- $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line));
+ $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line));
if ($template_type === false) {
throw new AssertionError(preg_last_error_msg());
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
index 9b769b5700a..54c42971897 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
@@ -903,6 +903,7 @@ private function createStorageForFunctionLike(
$storage->is_static = $stmt->isStatic();
$storage->final = $this->classlike_storage && $this->classlike_storage->final;
$storage->final_from_docblock = $this->classlike_storage && $this->classlike_storage->final_from_docblock;
+ $storage->visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC;
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
$cased_function_id =
($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name->name;
diff --git a/src/Psalm/Internal/PluginManager/ComposerLock.php b/src/Psalm/Internal/PluginManager/ComposerLock.php
index d806b09d81a..25808fe557b 100644
--- a/src/Psalm/Internal/PluginManager/ComposerLock.php
+++ b/src/Psalm/Internal/PluginManager/ComposerLock.php
@@ -18,7 +18,7 @@
/**
* @internal
*/
-final class ComposerLock
+class ComposerLock
{
/** @param string[] $file_names */
public function __construct(
diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php
index 83708e1cc00..cd853ce62d2 100644
--- a/src/Psalm/Internal/Type/ParseTreeCreator.php
+++ b/src/Psalm/Internal/Type/ParseTreeCreator.php
@@ -31,6 +31,7 @@
use function in_array;
use function preg_match;
use function strlen;
+use function strpos;
use function strtolower;
/**
@@ -139,6 +140,7 @@ public function create(): ParseTree
case 'is':
case 'as':
+ case 'of':
$this->handleIsOrAs($type_token);
break;
@@ -767,7 +769,7 @@ private function handleIsOrAs(array $type_token): void
array_pop($current_parent->children);
}
- if ($type_token[0] === 'as') {
+ if ($type_token[0] === 'as' || $type_token[0] == 'of') {
$next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null;
if (!$this->current_leaf instanceof Value
@@ -820,13 +822,30 @@ private function handleValue(array $type_token): void
break;
case '{':
+ ++$this->t;
+
+ $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null;
+
+ if ($nexter_token && strpos($nexter_token[0], '@') !== false) {
+ $this->t = $this->type_token_count;
+ if ($type_token[0] === '$this') {
+ $type_token[0] = 'static';
+ }
+
+ $new_leaf = new Value(
+ $type_token[0],
+ $type_token[1],
+ $type_token[1] + strlen($type_token[0]),
+ $type_token[2] ?? null,
+ $new_parent,
+ );
+ break;
+ }
+
$new_leaf = new KeyedArrayTree(
$type_token[0],
$new_parent,
);
- ++$this->t;
-
- $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null;
if ($nexter_token !== null && $nexter_token[0] === '}') {
$new_leaf->terminated = true;
diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php
index 51edb33080c..143255333c2 100644
--- a/src/Psalm/Internal/Type/TypeCombiner.php
+++ b/src/Psalm/Internal/Type/TypeCombiner.php
@@ -1515,7 +1515,7 @@ private static function getArrayTypeFromGenericParams(
$generic_type_params[1],
$objectlike_generic_type,
$codebase,
- $overwrite_empty_array,
+ false,
$allow_mixed_union,
);
}
diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php
index 57bb5c8b6c8..1b2dafe7031 100644
--- a/src/Psalm/Internal/Type/TypeTokenizer.php
+++ b/src/Psalm/Internal/Type/TypeTokenizer.php
@@ -9,9 +9,11 @@
use Psalm\Internal\Type\TypeAlias\InlineTypeAlias;
use Psalm\Type;
+use function array_slice;
use function array_splice;
use function array_unshift;
use function count;
+use function implode;
use function in_array;
use function is_numeric;
use function preg_match;
@@ -146,11 +148,9 @@ public static function tokenize(string $string_type, bool $ignore_space = true):
$type_tokens[++$rtc] = [' ', $i - 1];
$type_tokens[++$rtc] = ['', $i];
} elseif ($was_space
- && ($char === 'a' || $char === 'i')
- && ($chars[$i + 1] ?? null) === 's'
- && ($chars[$i + 2] ?? null) === ' '
+ && in_array(implode('', array_slice($chars, $i, 3)), ['as ', 'is ', 'of '])
) {
- $type_tokens[++$rtc] = [$char . 's', $i - 1];
+ $type_tokens[++$rtc] = [$char . $chars[$i+1], $i - 1];
$type_tokens[++$rtc] = ['', ++$i];
$was_char = false;
continue;
diff --git a/src/Psalm/Issue/MixedInferredReturnType.php b/src/Psalm/Issue/MixedInferredReturnType.php
deleted file mode 100644
index b3943899f12..00000000000
--- a/src/Psalm/Issue/MixedInferredReturnType.php
+++ /dev/null
@@ -1,13 +0,0 @@
-config->find_unused_issue_handler_suppression) {
+ foreach ($codebase->config->getIssueHandlers() as $type => $handler) {
+ foreach ($handler->getFilters() as $filter) {
+ if ($filter->suppressions > 0 && $filter->getErrorLevel() == Config::REPORT_SUPPRESS) {
+ continue;
+ }
+ $issues_data['config'][] = new IssueData(
+ IssueData::SEVERITY_ERROR,
+ 0,
+ 0,
+ UnusedIssueHandlerSuppression::getIssueType(),
+ sprintf(
+ 'Suppressed issue type "%s" for %s was not thrown.',
+ $type,
+ str_replace(
+ $codebase->config->base_dir,
+ '',
+ implode(', ', [...$filter->getFiles(), ...$filter->getDirectories()]),
+ ),
+ ),
+ $codebase->config->source_filename ?? '',
+ '',
+ '',
+ '',
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ UnusedIssueHandlerSuppression::SHORTCODE,
+ UnusedIssueHandlerSuppression::ERROR_LEVEL,
+ );
+ }
+ }
+ }
+
echo self::getOutput(
$issues_data,
$project_analyzer->stdout_report_options,
diff --git a/src/Psalm/Progress/DefaultProgress.php b/src/Psalm/Progress/DefaultProgress.php
index 57024cbf72f..64788f224cd 100644
--- a/src/Psalm/Progress/DefaultProgress.php
+++ b/src/Psalm/Progress/DefaultProgress.php
@@ -22,7 +22,7 @@ class DefaultProgress extends LongProgress
public function taskDone(int $level): void
{
- if ($this->number_of_tasks > self::TOO_MANY_FILES) {
+ if ($this->fixed_size && $this->number_of_tasks > self::TOO_MANY_FILES) {
++$this->progress;
// Source for rate limiting:
diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php
index 4bfa064d8e4..6184afab39d 100644
--- a/src/Psalm/Progress/LongProgress.php
+++ b/src/Psalm/Progress/LongProgress.php
@@ -21,22 +21,27 @@ class LongProgress extends Progress
protected int $progress = 0;
+ protected bool $fixed_size = false;
+
public function __construct(protected bool $print_errors = true, protected bool $print_infos = true)
{
}
public function startScanningFiles(): void
{
+ $this->fixed_size = false;
$this->write('Scanning files...' . "\n");
}
public function startAnalyzingFiles(): void
{
- $this->write('Analyzing files...' . "\n\n");
+ $this->fixed_size = true;
+ $this->write("\n" . 'Analyzing files...' . "\n\n");
}
public function startAlteringFiles(): void
{
+ $this->fixed_size = true;
$this->write('Altering files...' . "\n");
}
@@ -51,8 +56,30 @@ public function start(int $number_of_tasks): void
$this->progress = 0;
}
+ public function expand(int $number_of_tasks): void
+ {
+ $this->number_of_tasks += $number_of_tasks;
+ }
+
public function taskDone(int $level): void
{
+ if ($this->number_of_tasks === null) {
+ throw new LogicException('Progress::start() should be called before Progress::taskDone()');
+ }
+
+ ++$this->progress;
+
+ if (!$this->fixed_size) {
+ if ($this->progress == 1 || $this->progress == $this->number_of_tasks || $this->progress % 10 == 0) {
+ $this->write(sprintf(
+ "\r%s / %s?",
+ $this->progress,
+ $this->number_of_tasks,
+ ));
+ }
+ return;
+ }
+
if ($level === 0 || ($level === 1 && !$this->print_infos) || !$this->print_errors) {
$this->write(self::doesTerminalSupportUtf8() ? '░' : '_');
} elseif ($level === 1) {
@@ -61,7 +88,6 @@ public function taskDone(int $level): void
$this->write('E');
}
- ++$this->progress;
if (($this->progress % self::NUMBER_OF_COLUMNS) !== 0) {
return;
diff --git a/src/Psalm/Progress/Progress.php b/src/Psalm/Progress/Progress.php
index f6313214775..248878ff0a1 100644
--- a/src/Psalm/Progress/Progress.php
+++ b/src/Psalm/Progress/Progress.php
@@ -46,6 +46,10 @@ public function start(int $number_of_tasks): void
{
}
+ public function expand(int $number_of_tasks): void
+ {
+ }
+
public function taskDone(int $level): void
{
}
diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub
index 43a7bb1f85c..0e3a935bd78 100644
--- a/stubs/CoreGenericIterators.phpstub
+++ b/stubs/CoreGenericIterators.phpstub
@@ -477,7 +477,7 @@ class EmptyIterator implements Iterator {
}
/**
- * @template-extends SeekableIterator
+ * @template-extends DirectoryIterator
*/
class FilesystemIterator extends DirectoryIterator
{
@@ -523,7 +523,7 @@ class FilesystemIterator extends DirectoryIterator
/**
- * @template-extends SeekableIterator
+ * @template-extends FilesystemIterator
*/
class GlobIterator extends FilesystemIterator implements Countable {
/**
@@ -774,7 +774,7 @@ class RecursiveArrayIterator extends ArrayIterator implements RecursiveIterator
const CHILD_ARRAYS_ONLY = 4 ;
/**
- * @return RecursiveArrayIterator
+ * @return ?RecursiveArrayIterator
*/
public function getChildren() {}
diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub
index 3e86431e581..4007be3f007 100644
--- a/stubs/Reflection.phpstub
+++ b/stubs/Reflection.phpstub
@@ -496,7 +496,7 @@ class ReflectionProperty implements Reflector
public function isDefault(): bool {}
/**
- * @return int-mask-of
+ * @return int-mask-of
* @psalm-pure
*/
public function getModifiers(): int {}
diff --git a/stubs/extensions/soap.phpstub b/stubs/extensions/soap.phpstub
index 8a3fafa4dcd..dac3ece837c 100644
--- a/stubs/extensions/soap.phpstub
+++ b/stubs/extensions/soap.phpstub
@@ -1,5 +1,95 @@
[
+ 'code' => ' [
'code' => ' 'InvalidDocblock',
],
- 'noCrashOnInvalidClassTemplateAsType' => [
+ 'SKIPPED-noCrashOnInvalidClassTemplateAsType' => [
'code' => ' 'InvalidDocblock',
],
- 'noCrashOnInvalidFunctionTemplateAsType' => [
+ 'SKIPPED-noCrashOnInvalidFunctionTemplateAsType' => [
'code' => '
*/
class C implements ArrayAccess {
- public function offsetExists(int $offset) : bool { return true; }
+ public function offsetExists($offset) : bool { return true; }
public function offsetGet($offset) : string { return "";}
- public function offsetSet(?int $offset, string $value) : void {}
+ public function offsetSet($offset, string $value) : void {}
- public function offsetUnset(int $offset) : void { }
+ public function offsetUnset($offset) : void { }
}
$c = new C();
@@ -1964,13 +1964,13 @@ function foobar(): ?array
* @template-implements ArrayAccess
*/
class C implements ArrayAccess {
- public function offsetExists(int $offset) : bool { return true; }
+ public function offsetExists($offset) : bool { return true; }
public function offsetGet($offset) : string { return "";}
- public function offsetSet(int $offset, string $value) : void {}
+ public function offsetSet($offset, $value) : void {}
- public function offsetUnset(int $offset) : void { }
+ public function offsetUnset($offset) : void { }
}
$c = new C();
@@ -2048,6 +2048,15 @@ function getQueryParams(): array
return $queryParams;
}',
],
+ 'AssignListToNonEmptyList' => [
+ 'code' => '> $l*/
+ $l = [];
+ $l[] = [];',
+ 'assertions' => [
+ '$l===' => 'non-empty-array>',
+ ],
+ ],
];
}
@@ -2250,9 +2259,6 @@ function getCachedMixed(array $cache, string $locale) : string {
],
'mergeWithDeeplyNestedArray' => [
'code' => ' 'float|int',
],
],
+ 'incrementInLoop' => [
+ 'code' => ' [
+ '$i' => 'int<0, 10>',
+ '$j' => 'int<100, 110>',
+ ],
+ ],
+ 'decrementInLoop' => [
+ 'code' => ' 0; $i--) {
+ if (rand(0,1)) {
+ break;
+ }
+ }
+ for ($j = 110; $j > 100; $j--) {
+ if (rand(0,1)) {
+ break;
+ }
+ }',
+ 'assertions' => [
+ '$i' => 'int<0, 10>',
+ '$j' => 'int<100, 110>',
+ ],
+ ],
'coalesceFilterOutNullEvenWithTernary' => [
'code' => ' [
"UndefinedThisPropertyFetch: Instance property A::\$foo is not defined",
"MixedReturnStatement: Could not infer a return type",
- "MixedInferredReturnType: Could not verify return type 'string' for A::bar",
],
],
],
diff --git a/tests/CallableTest.php b/tests/CallableTest.php
index 4dc185ca6d2..5291961c975 100644
--- a/tests/CallableTest.php
+++ b/tests/CallableTest.php
@@ -1917,7 +1917,7 @@ public function bar($argOne, $argTwo)
}
}',
'error_message' => 'InvalidFunctionCall',
- 'ignored_issues' => ['UndefinedClass', 'MixedInferredReturnType'],
+ 'ignored_issues' => ['UndefinedClass'],
],
'undefinedCallableMethodFullString' => [
'code' => ' [],
'ignored_issues' => [
'UndefinedClass',
- 'MixedInferredReturnType',
'InvalidArgument',
],
],
@@ -356,7 +355,6 @@ function foo() : D {
'assertions' => [],
'ignored_issues' => [
'UndefinedClass',
- 'MixedInferredReturnType',
'InvalidArgument',
],
],
diff --git a/tests/DocblockInheritanceTest.php b/tests/DocblockInheritanceTest.php
index 792c7972b86..84c50b6366b 100644
--- a/tests/DocblockInheritanceTest.php
+++ b/tests/DocblockInheritanceTest.php
@@ -149,6 +149,32 @@ function takesF(F $f) : B {
return $f->map();
}',
],
+ 'inheritCorrectParamOnTypeChange' => [
+ 'code' => '|int $className */
+ public function a(array|int $className): int
+ {
+ return 0;
+ }
+ }
+
+ class B extends A
+ {
+ public function a(array|int|bool $className): int
+ {
+ return 0;
+ }
+ }
+
+ print_r((new A)->a(1));
+ print_r((new B)->a(true));
+ ',
+ 'assertions' => [],
+ 'ignored_issues' => [],
+ 'php_version' => '8.0',
+ ],
];
}
diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php
index d86293a8241..cee36ca3a30 100644
--- a/tests/DocumentationTest.php
+++ b/tests/DocumentationTest.php
@@ -18,6 +18,7 @@
use Psalm\Internal\Provider\Providers;
use Psalm\Internal\RuntimeCaches;
use Psalm\Issue\UnusedBaselineEntry;
+use Psalm\Issue\UnusedIssueHandlerSuppression;
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
use UnexpectedValueException;
@@ -270,6 +271,7 @@ public function providerInvalidCodeParse(): array
case 'TraitMethodSignatureMismatch':
case 'UncaughtThrowInGlobalScope':
case UnusedBaselineEntry::getIssueType():
+ case UnusedIssueHandlerSuppression::getIssueType():
continue 2;
/** @todo reinstate this test when the issue is restored */
@@ -288,10 +290,6 @@ public function providerInvalidCodeParse(): array
$ignored_issues = ['InvalidReturnStatement'];
break;
- case 'MixedInferredReturnType':
- $ignored_issues = ['MixedReturnStatement'];
- break;
-
case 'MixedStringOffsetAssignment':
$ignored_issues = ['MixedAssignment'];
break;
diff --git a/tests/EnumTest.php b/tests/EnumTest.php
index ebc67a05e1e..6991b89ae37 100644
--- a/tests/EnumTest.php
+++ b/tests/EnumTest.php
@@ -657,6 +657,28 @@ enum BarEnum: int {
'ignored_issues' => [],
'php_version' => '8.1',
],
+ 'stringBackedEnumCaseValueFromStringGlobalConstant' => [
+ 'code' => ' [],
+ 'ignored_issues' => [],
+ 'php_version' => '8.1',
+ ],
+ 'intBackedEnumCaseValueFromIntGlobalConstant' => [
+ 'code' => ' [],
+ 'ignored_issues' => [],
+ 'php_version' => '8.1',
+ ],
];
}
@@ -1107,6 +1129,50 @@ enum Bar: int
'ignored_issues' => [],
'php_version' => '8.1',
],
+ 'invalidStringBackedEnumCaseValueFromStringGlobalConstant' => [
+ 'code' => ' 'InvalidEnumCaseValue',
+ 'ignored_issues' => [],
+ 'php_version' => '8.1',
+ ],
+ 'invalidIntBackedEnumCaseValueFromIntGlobalConstant' => [
+ 'code' => ' 'InvalidEnumCaseValue',
+ 'ignored_issues' => [],
+ 'php_version' => '8.1',
+ ],
+ 'invalidStringBackedEnumCaseValueFromIntGlobalConstant' => [
+ 'code' => ' 'InvalidEnumCaseValue',
+ 'ignored_issues' => [],
+ 'php_version' => '8.1',
+ ],
+ 'invalidIntBackedEnumCaseValueFromStringGlobalConstant' => [
+ 'code' => ' 'InvalidEnumCaseValue',
+ 'ignored_issues' => [],
+ 'php_version' => '8.1',
+ ],
];
}
}
diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php
index d012e72fa9e..a9599e5e9e9 100644
--- a/tests/FunctionCallTest.php
+++ b/tests/FunctionCallTest.php
@@ -917,7 +917,7 @@ function portismaybeint(string $s) : ? int {
'$porta' => 'false|int|null',
'$porte' => 'false|int|null',
],
- 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
+ 'ignored_issues' => ['MixedReturnStatement'],
],
'parseUrlComponent' => [
'code' => ' 5,
+ 'error_count' => 4,
'message' => 'Cannot find referenced variable $b',
'line' => 3,
'error' => '$b',
@@ -100,7 +100,7 @@ function fooFoo(int $a): int {
function fooFoo(Badger\Bodger $a): Badger\Bodger {
return $a;
}',
- 'error_count' => 3,
+ 'error_count' => 2,
'message' => 'Class, interface or enum named Badger\\Bodger does not exist',
'line' => 2,
'error' => 'Badger\\Bodger',
diff --git a/tests/Loop/ForTest.php b/tests/Loop/ForTest.php
index f8fd5f2b22b..037893b1ef4 100644
--- a/tests/Loop/ForTest.php
+++ b/tests/Loop/ForTest.php
@@ -143,7 +143,7 @@ function test(Node $head) {
* @param list $arr
*/
function cartesianProduct(array $arr) : void {
- for ($i = 20; $arr[$i] === 5 && $i > 0; $i--) {}
+ for ($i = 20; $i > 0 && $arr[$i] === 5 ; $i--) {}
}',
],
'noCrashOnLongThing' => [
diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php
index ad157dad39b..e6737ab0d78 100644
--- a/tests/MagicMethodAnnotationTest.php
+++ b/tests/MagicMethodAnnotationTest.php
@@ -824,7 +824,7 @@ function consumeInt(int $i): void {}
'callUsingParent' => [
'code' => ' [
+ 'code' => ' [],
+ 'ignored_issues' => ['ParamNameMismatch'],
+ ],
];
}
@@ -1118,6 +1133,21 @@ class B extends A {}
$b->foo();',
'error_message' => 'UndefinedMagicMethod',
],
+ 'inheritSealedMethodsWithoutPrefix' => [
+ 'code' => 'foo();',
+ 'error_message' => 'UndefinedMagicMethod',
+ ],
'lonelyMethod' => [
'code' => ' 'UndefinedVariable',
],
+ 'MagicMethodReturnTypesCheckedForClasses' => [
+ 'code' => ' 'ImplementedReturnTypeMismatch',
+ ],
+ 'MagicMethodParamTypesCheckedForClasses' => [
+ 'code' => ' 'ImplementedParamTypeMismatch',
+ ],
+ 'MagicMethodReturnTypesCheckedForInterfaces' => [
+ 'code' => ' 'ImplementedReturnTypeMismatch',
+ ],
+ 'MagicMethodParamTypesCheckedForInterfaces' => [
+ 'code' => ' 'ImplementedParamTypeMismatch',
+ ],
+ 'MagicMethodMadeConcreteChecksParams' => [
+ 'code' => ' 'ImplementedParamTypeMismatch',
+ ],
];
}
diff --git a/tests/MagicPropertyTest.php b/tests/MagicPropertyTest.php
index d0e340719a5..abb03aed1a0 100644
--- a/tests/MagicPropertyTest.php
+++ b/tests/MagicPropertyTest.php
@@ -398,7 +398,7 @@ public function __get(string $name) : string {
}
}',
'assertions' => [],
- 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
+ 'ignored_issues' => ['MixedReturnStatement'],
],
'overrideInheritedProperty' => [
'code' => ' 'InvalidDocblock',
],
+ 'sealedWithNoProperties' => [
+ 'code' => 'errors;',
+ 'error_message' => 'UndefinedMagicPropertyFetch',
+ ],
+ 'sealedWithNoPropertiesNoPrefix' => [
+ 'code' => 'errors;',
+ 'error_message' => 'UndefinedMagicPropertyFetch',
+ ],
];
}
diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php
index f77eb36203e..f0ae5b7db77 100644
--- a/tests/MethodCallTest.php
+++ b/tests/MethodCallTest.php
@@ -1150,7 +1150,7 @@ public static function createFromInterface(\DateTimeInterface $datetime): static
}
}',
'assertions' => [],
- 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
+ 'ignored_issues' => ['MixedReturnStatement'],
'php_version' => '8.0',
],
'nullsafeShortCircuit' => [
@@ -1342,7 +1342,7 @@ public function returns_nullable_class() {
}
}',
'error_message' => 'LessSpecificReturnStatement',
- 'ignored_issues' => ['MixedInferredReturnType', 'MixedReturnStatement', 'MixedMethodCall'],
+ 'ignored_issues' => ['MixedReturnStatement', 'MixedMethodCall'],
],
'undefinedVariableStaticCall' => [
'code' => ' 'B',
],
],
+ 'returnIgnoresInlineComments' => [
+ 'code' => ' [
'code' => ' [],
+ 'ignored_errors' => [],
+ 'php_version' => '8.0',
],
'doesNotRequireInterfaceDestructorsToHaveReturnType' => [
'code' => ' 'MethodSignatureMismatch',
],
+ 'methodAnnotationReturnMismatch' => [
+ 'code' => ' 'MismatchingDocblockReturnType',
+ ],
+ 'methodAnnotationParamMismatch' => [
+ 'code' => ' 'MismatchingDocblockParamType',
+ ],
];
}
}
diff --git a/tests/MixinAnnotationTest.php b/tests/MixinAnnotationTest.php
index f4f0372cd13..807fc4a57a5 100644
--- a/tests/MixinAnnotationTest.php
+++ b/tests/MixinAnnotationTest.php
@@ -596,6 +596,28 @@ class FooModel extends Model {}
'$g' => 'list',
],
],
+ 'mixinInheritMagicMethods' => [
+ 'code' => 'active();',
+ 'assertions' => [
+ '$c' => 'B',
+ ],
+ ],
];
}
diff --git a/tests/ReferenceConstraintTest.php b/tests/ReferenceConstraintTest.php
index 9fcd325ac7f..9b7811961e5 100644
--- a/tests/ReferenceConstraintTest.php
+++ b/tests/ReferenceConstraintTest.php
@@ -81,7 +81,6 @@ function testRef() : array {
'MixedAssignment',
'MixedArrayAccess',
'MixedReturnStatement',
- 'MixedInferredReturnType',
'MixedOperand',
],
],
diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php
index c17370a8106..79f10443c30 100644
--- a/tests/ReportOutputTest.php
+++ b/tests/ReportOutputTest.php
@@ -758,28 +758,6 @@ public function testJsonReport(): void
[
'link' => 'https://psalm.dev/047',
'severity' => 'error',
- 'line_from' => 2,
- 'line_to' => 2,
- 'type' => 'MixedInferredReturnType',
- 'message' => 'Could not verify return type \'null|string\' for psalmCanVerify',
- 'file_name' => 'somefile.php',
- 'file_path' => 'somefile.php',
- 'snippet' => 'function psalmCanVerify(int $your_code): ?string {',
- 'selected_text' => '?string',
- 'from' => 47,
- 'to' => 54,
- 'snippet_from' => 6,
- 'snippet_to' => 56,
- 'column_from' => 42,
- 'column_to' => 49,
- 'shortcode' => 47,
- 'error_level' => 1,
- 'taint_trace' => null,
- 'other_references' => null,
- ],
- [
- 'link' => 'https://psalm.dev/020',
- 'severity' => 'error',
'line_from' => 8,
'line_to' => 8,
'type' => 'UndefinedConstant',
@@ -854,7 +832,7 @@ public function testFilteredJsonReportIsStillArray(): void
];
$report_options = ProjectAnalyzer::getFileReportOptions([__DIR__ . '/test-report.json'])[0];
- $fixable_issue_counts = ['MixedInferredReturnType' => 1];
+ $fixable_issue_counts = [];
$report = new JsonReport(
$issues_data,
@@ -902,22 +880,6 @@ public function testSonarqubeReport(): void
'type' => 'CODE_SMELL',
'severity' => 'CRITICAL',
],
- [
- 'engineId' => 'Psalm',
- 'ruleId' => 'MixedInferredReturnType',
- 'primaryLocation' => [
- 'message' => 'Could not verify return type \'null|string\' for psalmCanVerify',
- 'filePath' => 'somefile.php',
- 'textRange' => [
- 'startLine' => 2,
- 'endLine' => 2,
- 'startColumn' => 41,
- 'endColumn' => 48,
- ],
- ],
- 'type' => 'CODE_SMELL',
- 'severity' => 'CRITICAL',
- ],
[
'engineId' => 'Psalm',
'ruleId' => 'UndefinedConstant',
@@ -972,7 +934,6 @@ public function testEmacsReport(): void
<<<'EOF'
somefile.php:3:10:error - UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024)
somefile.php:3:10:error - MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
- somefile.php:2:42:error - MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047)
somefile.php:8:6:error - UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020)
somefile.php:17:6:warning - PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126)
@@ -991,7 +952,6 @@ public function testPylintReport(): void
<<<'EOF'
somefile.php:3: [E0001] UndefinedVariable: Cannot find referenced variable $as_you_____type (column 10)
somefile.php:3: [E0001] MixedReturnStatement: Could not infer a return type (column 10)
- somefile.php:2: [E0001] MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (column 42)
somefile.php:8: [E0001] UndefinedConstant: Const CHANGE_ME is not defined (column 6)
somefile.php:17: [W0001] PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (column 6)
@@ -1015,9 +975,6 @@ public function testConsoleReport(): void
ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138)
return $as_you_____type;
- ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047)
- function psalmCanVerify(int $your_code): ?string {
-
ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020)
echo CHANGE_ME;
@@ -1046,9 +1003,6 @@ public function testConsoleReportNoInfo(): void
ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138)
return $as_you_____type;
- ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047)
- function psalmCanVerify(int $your_code): ?string {
-
ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020)
echo CHANGE_ME;
@@ -1074,9 +1028,6 @@ public function testConsoleReportNoSnippet(): void
ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138)
- ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047)
-
-
ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020)
@@ -1135,15 +1086,14 @@ public function testCompactReport(): void
<<<'EOF'
FILE: somefile.php
- +----------+------+---------------------------------+---------------------------------------------------------------+
- | SEVERITY | LINE | ISSUE | DESCRIPTION |
- +----------+------+---------------------------------+---------------------------------------------------------------+
- | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type |
- | ERROR | 3 | MixedReturnStatement | Could not infer a return type |
- | ERROR | 2 | MixedInferredReturnType | Could not verify return type 'null|string' for psalmCanVerify |
- | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined |
- | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 |
- +----------+------+---------------------------------+---------------------------------------------------------------+
+ +----------+------+---------------------------------+--------------------------------------------------------------+
+ | SEVERITY | LINE | ISSUE | DESCRIPTION |
+ +----------+------+---------------------------------+--------------------------------------------------------------+
+ | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type |
+ | ERROR | 3 | MixedReturnStatement | Could not infer a return type |
+ | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined |
+ | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 |
+ +----------+------+---------------------------------+--------------------------------------------------------------+
EOF,
$this->toUnixLineEndings(IssueBuffer::getOutput(IssueBuffer::getIssuesData(), $compact_report_options)),
@@ -1166,9 +1116,6 @@ public function testCheckstyleReport(): void
-
-
-
@@ -1199,8 +1146,8 @@ public function testJunitReport(): void
$this->assertSame(
<<<'EOF'
-
-
+
+
message: Cannot find referenced variable $as_you_____type
type: UndefinedVariable
@@ -1219,16 +1166,6 @@ public function testJunitReport(): void
line: 3
column_from: 10
column_to: 26
-
-
-
- message: Could not verify return type 'null|string' for psalmCanVerify
- type: MixedInferredReturnType
- snippet: function psalmCanVerify(int $your_code): ?string {
- selected_text: ?string
- line: 2
- column_from: 42
- column_to: 49
@@ -1283,7 +1220,6 @@ public function testGithubActionsOutput(): void
$expected_output = <<<'EOF'
::error file=somefile.php,line=3,col=10,title=UndefinedVariable::somefile.php:3:10: UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024)
::error file=somefile.php,line=3,col=10,title=MixedReturnStatement::somefile.php:3:10: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
- ::error file=somefile.php,line=2,col=42,title=MixedInferredReturnType::somefile.php:2:42: MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047)
::error file=somefile.php,line=8,col=6,title=UndefinedConstant::somefile.php:8:6: UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020)
::warning file=somefile.php,line=17,col=6,title=PossiblyUndefinedGlobalVariable::somefile.php:17:6: PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126)
@@ -1301,7 +1237,6 @@ public function testCountOutput(): void
$report_options = new ReportOptions();
$report_options->format = Report::TYPE_COUNT;
$expected_output = <<<'EOF'
- MixedInferredReturnType: 1
MixedReturnStatement: 1
PossiblyUndefinedGlobalVariable: 1
UndefinedConstant: 1
diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php
index 41d93d12453..0cfc5f52c89 100644
--- a/tests/ReturnTypeTest.php
+++ b/tests/ReturnTypeTest.php
@@ -1380,14 +1380,6 @@ function fooFoo() {
}',
'error_message' => 'MissingReturnType',
],
- 'mixedInferredReturnType' => [
- 'code' => ' 'MixedInferredReturnType',
- ],
'mixedInferredReturnStatement' => [
'code' => ' 'MixedReturnStatement',
],
- 'invalidReturnTypeClass' => [
- 'code' => ' 'UndefinedClass',
- 'ignored_issues' => ['MixedInferredReturnType'],
- ],
'invalidClassOnCall' => [
'code' => 'bar();',
'error_message' => 'UndefinedClass',
- 'ignored_issues' => ['MixedInferredReturnType', 'MixedReturnStatement'],
+ 'ignored_issues' => ['MixedReturnStatement'],
],
'returnArrayOfNullableInvalid' => [
'code' => ' $className
* @psalm-return RequestedType&MockObject
- * @psalm-suppress MixedInferredReturnType
* @psalm-suppress MixedReturnStatement
*/
function mockHelper(string $className)
@@ -444,7 +443,6 @@ public function checkExpectations() : void
* @psalm-template RequestedType
* @psalm-param class-string $className
* @psalm-return RequestedType&MockObject
- * @psalm-suppress MixedInferredReturnType
* @psalm-suppress MixedReturnStatement
*/
function mockHelper(string $className)
@@ -482,7 +480,6 @@ public function checkExpectations() : void
* @psalm-template RequestedType
* @psalm-param class-string $className
* @psalm-return MockObject&RequestedType
- * @psalm-suppress MixedInferredReturnType
* @psalm-suppress MixedReturnStatement
*/
function mockHelper(string $className)
diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php
index 64210ed4bbc..efb5123e0c3 100644
--- a/tests/Template/FunctionTemplateTest.php
+++ b/tests/Template/FunctionTemplateTest.php
@@ -1336,7 +1336,6 @@ function foo(Closure $fn, $arg): void {
* @param E $e
* @param mixed $d
* @return ?E
- * @psalm-suppress MixedInferredReturnType
*/
function reduce_values($e, $d) {
if (rand(0, 1)) {
@@ -1359,7 +1358,6 @@ function reduce_values($e, $d) {
* @param E $e
* @param mixed $d
* @return ?E
- * @psalm-suppress MixedInferredReturnType
*/
function reduce_values($e, $d)
{
diff --git a/tests/Template/TraitTemplateTest.php b/tests/Template/TraitTemplateTest.php
index 86cf5d8f022..7074c24ea10 100644
--- a/tests/Template/TraitTemplateTest.php
+++ b/tests/Template/TraitTemplateTest.php
@@ -168,6 +168,32 @@ class B {
use T;
}',
],
+ 'multilineTemplateUse' => [
+ 'code' => '
+ */
+ use MyTrait;
+ }
+
+ class Bar {
+ /**
+ * @template-use MyTrait
+ */
+ use MyTrait;
+ }',
+ ],
'allowTraitExtendAndImplementWithExplicitParamType' => [
'code' => ' 'array{phone: string}',
],
],
+ 'multilineTypeWithExtraSpace' => [
+ 'code' => ' [
'code' => 'assertSame(
+ 'class-string-map',
+ Type::parseString('class-string-map')->getId(false),
+ );
+ }
+
public function testVeryLargeType(): void
{
$very_large_type = 'array{a: Closure():(array|null), b?: Closure():array, c?: Closure():array, d?: Closure():array, e?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), p?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), q: string, r?: Closure():(array|null), s: array}|null';
diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php
index 4c96c6783f9..6ed17a19da3 100644
--- a/tests/TypeReconciliation/ArrayKeyExistsTest.php
+++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php
@@ -79,7 +79,7 @@ public function bar(string $key): bool {
}
}',
'assertions' => [],
- 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
+ 'ignored_issues' => ['MixedReturnStatement'],
],
'assertSelfClassConstantOffsetsInFunction' => [
'code' => ' [],
- 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
+ 'ignored_issues' => ['MixedReturnStatement'],
],
'assertNamedClassConstantOffsetsInFunction' => [
'code' => ' [],
- 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
+ 'ignored_issues' => ['MixedReturnStatement'],
],
'possiblyUndefinedArrayAccessWithArrayKeyExists' => [
'code' => ' [
'code' => ' ' [
+ 'code' => ' [
+ 'code' => ' [
+ 'code' => ' [
'code' => ' [],
- 'ignored_issues' => ['MixedInferredReturnType'],
],
'grandParentInstanceofConfusion' => [
'code' => ' [
'code' => ' '