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' => ' '