diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90e3705cf4c..28480bab993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,6 +135,7 @@ jobs: CHUNK_COUNT: "${{ matrix.count }}" CHUNK_NUMBER: "${{ matrix.chunk }}" PARALLEL_PROCESSES: 5 + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} steps: - name: Set up PHP diff --git a/bin/tests-github-actions.sh b/bin/tests-github-actions.sh index 4296f70591a..2c0ef4b787b 100755 --- a/bin/tests-github-actions.sh +++ b/bin/tests-github-actions.sh @@ -3,13 +3,15 @@ set -eu function get_seeded_random() { - openssl enc -aes-256-ctr -pass pass:"vimeo/psalm" -nosalt /dev/null + local -r branch_name="$1" + openssl enc -aes-256-ctr -pass pass:"$branch_name" -nosalt /dev/null } function run { local -r chunk_count="$1" local -r chunk_number="$2" local -r parallel_processes="$3" + local -r branch_name="$4" local -r phpunit_cmd=' echo "::group::{}"; @@ -23,7 +25,7 @@ exit "$exit_code"' mkdir -p build/parallel/ build/phpunit/logs/ - find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random) > build/tests_all + find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random "$branch_name") > build/tests_all # split incorrectly splits the lines by byte size, which means that the number of tests per file are as evenly distributed as possible #split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split local -r lines=$(wc -l ['string', 'num'=>'int'], 'dechex' => ['string', 'num'=>'int'], 'decoct' => ['string', 'num'=>'int'], -'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'], +'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'], 'define_syslog_variables' => ['void'], 'defined' => ['bool', 'constant_name'=>'string'], 'deflate_add' => ['string|false', 'context'=>'DeflateContext', 'data'=>'string', 'flush_mode='=>'int'], @@ -12923,8 +12923,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'?int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], -'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], -'strtok\'1' => ['string|false', 'string'=>'string'], +'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], +'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'?int'], 'strtoupper' => ['string', 'string'=>'string'], diff --git a/dictionaries/CallMap_73_delta.php b/dictionaries/CallMap_73_delta.php index d79026c3805..6352fa06292 100644 --- a/dictionaries/CallMap_73_delta.php +++ b/dictionaries/CallMap_73_delta.php @@ -64,6 +64,10 @@ 'old' => ['int', 'scale'=>'int'], 'new' => ['int', 'scale='=>'int'], ], + 'define' => [ + 'old' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'], + 'new' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'], + ], 'ldap_compare' => [ 'old' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string'], 'new' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string', 'controls='=>'array'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index afb21ba72df..60e798e09cb 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -9937,7 +9937,7 @@ 'decbin' => ['string', 'num'=>'int'], 'dechex' => ['string', 'num'=>'int'], 'decoct' => ['string', 'num'=>'int'], - 'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'], + 'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'], 'define_syslog_variables' => ['void'], 'defined' => ['bool', 'constant_name'=>'string'], 'deflate_add' => ['string|false', 'context'=>'resource', 'data'=>'string', 'flush_mode='=>'int'], @@ -14333,8 +14333,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], - 'strtok\'1' => ['string|false', 'string'=>'string'], + 'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], + 'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'int'], 'strtoupper' => ['string', 'string'=>'string'], diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 9728c570c20..1926d014f4c 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -219,13 +219,15 @@ + + $token_list[$iter] + $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[0] - $token_list[1] @@ -233,6 +235,11 @@ expr->getArgs()[0]]]> + + + + + $identifier_name @@ -378,6 +385,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 9c188d9e0f2..c248e3e0574 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1160,7 +1160,7 @@ private static function fromXmlAndPaths( } // we need an absolute path for checks - if ($path[0] !== '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isRelative($path)) { $prospective_path = $base_dir . DIRECTORY_SEPARATOR . $path; } else { $prospective_path = $path; diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index 4eb8c1a32f2..26338386df9 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -9,6 +9,7 @@ use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use SimpleXMLElement; +use Symfony\Component\Filesystem\Path; use function array_filter; use function array_map; @@ -127,7 +128,7 @@ public static function loadFromArray( $resolve_symlinks = (bool) ($directory['resolveSymlinks'] ?? false); $declare_strict_types = (bool) ($directory['useStrictTypes'] ?? false); - if ($directory_path[0] === '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isAbsolute($directory_path)) { /** @var non-empty-string */ $prospective_directory_path = $directory_path; } else { @@ -250,7 +251,7 @@ public static function loadFromArray( foreach ($config['file'] as $file) { $file_path = (string) ($file['name'] ?? ''); - if ($file_path[0] === '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isAbsolute($file_path)) { /** @var non-empty-string */ $prospective_file_path = $file_path; } else { diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 34cd81bc0fb..f42056bc465 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -15,7 +15,6 @@ use Psalm\Issue\UndefinedVariable; use Psalm\IssueBuffer; use Psalm\Type; -use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; @@ -135,12 +134,6 @@ public static function analyzeExpression( $use_var_id = '$' . $use->var->name; - // insert the ref into the current context if passed by ref, as whatever we're passing - // the closure to could execute it straight away. - if ($use->byRef && !$context->hasVariable($use_var_id)) { - $context->vars_in_scope[$use_var_id] = new Union([new TMixed()], ['by_ref' => true]); - } - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph && $context->hasVariable($use_var_id) ) { @@ -156,7 +149,7 @@ public static function analyzeExpression( } $use_context->vars_in_scope[$use_var_id] = - $context->hasVariable($use_var_id) && !$use->byRef + $context->hasVariable($use_var_id) ? $context->vars_in_scope[$use_var_id] : Type::getMixed(); @@ -207,7 +200,12 @@ public static function analyzeExpression( $use_context->calling_method_id = $context->calling_method_id; $use_context->phantom_classes = $context->phantom_classes; - $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false); + $byref_vars = []; + $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false, $byref_vars); + + foreach ($byref_vars as $key => $value) { + $context->vars_in_scope[$key] = $value; + } if ($closure_analyzer->inferred_impure && $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer @@ -231,7 +229,7 @@ public static function analyzeExpression( /** * @return false|null */ - public static function analyzeClosureUses( + private static function analyzeClosureUses( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Closure $stmt, Context $context, @@ -270,21 +268,6 @@ public static function analyzeClosureUses( continue; } - if ($use->byRef) { - $context->vars_in_scope[$use_var_id] = Type::getMixed(); - $context->vars_possibly_in_scope[$use_var_id] = true; - - if (!$statements_analyzer->hasVariable($use_var_id)) { - $statements_analyzer->registerVariable( - $use_var_id, - new CodeLocation($statements_analyzer, $use->var), - null, - ); - } - - return null; - } - if (!isset($context->vars_possibly_in_scope[$use_var_id])) { if ($context->check_variables) { if (IssueBuffer::accepts( @@ -331,14 +314,6 @@ public static function analyzeClosureUses( continue; } - } elseif ($use->byRef) { - $new_type = new Union([new TMixed()], [ - 'parent_nodes' => $context->vars_in_scope[$use_var_id]->parent_nodes, - ]); - - $context->remove($use_var_id); - - $context->vars_in_scope[$use_var_id] = $new_type; } } diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 80bf330b332..35ab5488bb9 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -47,6 +47,8 @@ use Psalm\Issue\UnusedDocblockParam; use Psalm\Issue\UnusedParam; use Psalm\IssueBuffer; +use Psalm\Node\Expr\VirtualVariable; +use Psalm\Node\Stmt\VirtualWhile; use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; @@ -151,14 +153,18 @@ public function __construct($function, SourceAnalyzer $source, FunctionLikeStora /** * @param bool $add_mutations whether or not to add mutations to this method + * @param array $byref_vars + * @param-out array $byref_vars * @return false|null * @psalm-suppress PossiblyUnusedReturnValue unused but seems important + * @psalm-suppress ComplexMethod Unavoidably complex */ public function analyze( Context $context, NodeDataProvider $type_provider, ?Context $global_context = null, bool $add_mutations = false, + array &$byref_vars = [] ): ?bool { $storage = $this->storage; @@ -188,7 +194,13 @@ public function analyze( || !in_array("UnusedPsalmSuppress", $storage->suppressed_issues) ) { foreach ($storage->suppressed_issues as $offset => $issue_name) { - IssueBuffer::addUnusedSuppression($this->getFilePath(), $offset, $issue_name); + IssueBuffer::addUnusedSuppression( + $storage->location !== null + ? $storage->location->file_path + : $this->getFilePath(), + $offset, + $issue_name, + ); } } } @@ -231,9 +243,8 @@ public function analyze( $statements_analyzer = new StatementsAnalyzer($this, $type_provider); + $byref_uses = []; if ($this instanceof ClosureAnalyzer && $this->function instanceof Closure) { - $byref_uses = []; - foreach ($this->function->uses as $use) { if (!is_string($use->var->name)) { continue; @@ -348,6 +359,31 @@ public function analyze( (bool) $template_types, ); + if ($byref_uses) { + $ref_context = clone $context; + $var = '$__tmp_byref_closure_if__' . (int) $this->function->getAttribute('startFilePos'); + + $ref_context->vars_in_scope[$var] = Type::getBool(); + + $var = new VirtualVariable( + substr($var, 1), + ); + $virtual_while = new VirtualWhile( + $var, + $function_stmts, + ); + + $statements_analyzer->analyze( + [$virtual_while], + $ref_context, + ); + + foreach ($byref_uses as $var_id => $_) { + $byref_vars[$var_id] = $ref_context->vars_in_scope[$var_id]; + $context->vars_in_scope[$var_id] = $ref_context->vars_in_scope[$var_id]; + } + } + if ($storage->pure) { $context->pure = true; } diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index be4ca33a212..bfec8e449fc 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1067,6 +1067,10 @@ public function checkPaths(array $paths_to_check): void $this->config->visitStubFiles($this->codebase, $this->progress); + $event = new AfterCodebasePopulatedEvent($this->codebase); + + $this->config->eventDispatcher->dispatchAfterCodebasePopulated($event); + $this->progress->startAnalyzingFiles(); $this->codebase->analyzer->analyzeFiles( diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index 2168ed3f956..fa32fdd181f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -36,14 +36,11 @@ use function array_keys; use function array_merge; use function array_reduce; -use function array_unique; use function count; use function in_array; use function preg_match; use function preg_quote; use function spl_object_id; -use function strpos; -use function substr; /** * @internal @@ -274,20 +271,6 @@ public static function analyze( array_keys($if_scope->negated_types), ); - $extra_vars_to_update = []; - - // if there's an object-like array in there, we also need to update the root array variable - foreach ($vars_to_update as $var_id) { - $bracked_pos = strpos($var_id, '['); - if ($bracked_pos !== false) { - $extra_vars_to_update[] = substr($var_id, 0, $bracked_pos); - } - } - - if ($extra_vars_to_update) { - $vars_to_update = array_unique(array_merge($extra_vars_to_update, $vars_to_update)); - } - $outer_context->update( $old_if_context, $if_context, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 16ea3e83561..70f35407eb1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -543,9 +543,9 @@ private static function scrapeEqualityAssertions( ); } else { // both side of the Identical can be asserted to the intersection of both - $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase); + $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase, false, false); - if ($intersection_type !== null && $intersection_type->isSingle()) { + if ($intersection_type !== null) { $if_types = []; $var_name_left = ExpressionIdentifier::getExtendedVarId( @@ -556,8 +556,13 @@ private static function scrapeEqualityAssertions( $var_assertion_different = $var_type->getId() !== $intersection_type->getId(); + $all_assertions = []; + foreach ($intersection_type->getAtomicTypes() as $atomic_type) { + $all_assertions[] = new IsIdentical($atomic_type); + } + if ($var_name_left && $var_assertion_different) { - $if_types[$var_name_left] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_left] = [$all_assertions]; } $var_name_right = ExpressionIdentifier::getExtendedVarId( @@ -569,7 +574,7 @@ private static function scrapeEqualityAssertions( $other_assertion_different = $other_type->getId() !== $intersection_type->getId(); if ($var_name_right && $other_assertion_different) { - $if_types[$var_name_right] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_right] = [$all_assertions]; } return $if_types ? [$if_types] : []; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 3a47c43d894..2c23c6951b0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -346,29 +346,25 @@ private static function updateTypeWithKeyValues( } if (!$has_matching_objectlike_property && !$has_matching_string) { - if (count($key_values) === 1) { - $key_value = $key_values[0]; - - $object_like = new TKeyedArray( - [$key_value->value => $current_type], - $key_value instanceof TLiteralClassString - ? [$key_value->value => true] - : null, - ); - - $array_assignment_type = new Union([ - $object_like, - ]); - } else { - $array_assignment_literals = $key_values; - - $array_assignment_type = new Union([ - new TNonEmptyArray([ - new Union($array_assignment_literals), - $current_type, - ]), - ]); + $properties = []; + $classStrings = []; + $current_type = $current_type->setPossiblyUndefined( + $current_type->possibly_undefined || count($key_values) > 1, + ); + foreach ($key_values as $key_value) { + $properties[$key_value->value] = $current_type; + if ($key_value instanceof TLiteralClassString) { + $classStrings[$key_value->value] = true; + } } + $object_like = new TKeyedArray( + $properties, + $classStrings ?: null, + ); + + $array_assignment_type = new Union([ + $object_like, + ]); return Type::combineUnionTypes( $child_stmt_type, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php index 364a9ec7ef5..e50f0bb55e2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php @@ -41,6 +41,7 @@ public static function analyze( || $root_expr instanceof PhpParser\Node\Expr\MethodCall || $root_expr instanceof PhpParser\Node\Expr\StaticCall || $root_expr instanceof PhpParser\Node\Expr\Cast + || $root_expr instanceof PhpParser\Node\Expr\Match_ || $root_expr instanceof PhpParser\Node\Expr\NullsafePropertyFetch || $root_expr instanceof PhpParser\Node\Expr\NullsafeMethodCall || $root_expr instanceof PhpParser\Node\Expr\Ternary diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 7e1c1a9758d..d92d3c48b98 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -896,7 +896,7 @@ public static function verifyType( $input_type, $param_type, true, - true, + !isset($param_type->getAtomicTypes()['true']), $union_comparison_results, ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index ba425e189d9..9b5b357e26d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -405,13 +405,6 @@ public static function analyze( } } - if ($function_call_info->byref_uses) { - foreach ($function_call_info->byref_uses as $byref_use_var => $_) { - $context->vars_in_scope['$' . $byref_use_var] = Type::getMixed(); - $context->vars_possibly_in_scope['$' . $byref_use_var] = true; - } - } - if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { NamedFunctionCallHandler::handle( $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 2d35a9c8168..8aedd37da5a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -51,6 +51,7 @@ use function strpos; use function strtolower; use function substr; +use function trim; /** * @internal @@ -631,17 +632,19 @@ private static function taintReturnType( $first_arg_value = $first_stmt_type->getSingleStringLiteral()->value; $pattern = substr($first_arg_value, 1, -1); + if (strlen(trim($pattern)) > 0) { + $pattern = trim($pattern); + if ($pattern[0] === '[' + && $pattern[1] === '^' + && substr($pattern, -1) === ']' + ) { + $pattern = substr($pattern, 2, -1); - if ($pattern[0] === '[' - && $pattern[1] === '^' - && substr($pattern, -1) === ']' - ) { - $pattern = substr($pattern, 2, -1); - - if (self::simpleExclusion($pattern, $first_arg_value[0])) { - $removed_taints[] = 'html'; - $removed_taints[] = 'has_quotes'; - $removed_taints[] = 'sql'; + if (self::simpleExclusion($pattern, $first_arg_value[0])) { + $removed_taints[] = 'html'; + $removed_taints[] = 'has_quotes'; + $removed_taints[] = 'sql'; + } } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index a4f0c47d864..c5409880ba3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -227,7 +227,38 @@ public static function handle( } if ($function_id === 'defined') { - $context->check_consts = false; + if ($first_arg && !$context->inside_negation) { + $fq_const_name = ConstFetchAnalyzer::getConstName( + $first_arg->value, + $statements_analyzer->node_data, + $codebase, + $statements_analyzer->getAliases(), + ); + + if ($fq_const_name !== null) { + $const_type = ConstFetchAnalyzer::getConstType( + $statements_analyzer, + $fq_const_name, + true, + $context, + ); + + if (!$const_type) { + ConstFetchAnalyzer::setConstType( + $statements_analyzer, + $fq_const_name, + Type::getMixed(), + $context, + ); + + $context->check_consts = false; + } + } else { + $context->check_consts = false; + } + } else { + $context->check_consts = false; + } return; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index cfc1fedaacc..21e844b0718 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -568,12 +568,12 @@ private static function handleNamedCall( true, $context->insideUse(), )) { - $callstatic_appearing_id = $codebase->methods->getAppearingMethodId($callstatic_id); - assert($callstatic_appearing_id !== null); + $callstatic_declaring_id = $codebase->methods->getDeclaringMethodId($callstatic_id); + assert($callstatic_declaring_id !== null); $callstatic_pure = false; $callstatic_mutation_free = false; - if ($codebase->methods->hasStorage($callstatic_appearing_id)) { - $callstatic_storage = $codebase->methods->getStorage($callstatic_appearing_id); + if ($codebase->methods->hasStorage($callstatic_declaring_id)) { + $callstatic_storage = $codebase->methods->getStorage($callstatic_declaring_id); $callstatic_pure = $callstatic_storage->pure; $callstatic_mutation_free = $callstatic_storage->mutation_free; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 83123189023..c94e1d09a14 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -22,6 +22,7 @@ use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type\TaintKind; +use Symfony\Component\Filesystem\Path; use function constant; use function defined; @@ -95,13 +96,7 @@ public static function analyze( $include_path = self::resolveIncludePath($path_to_file, dirname($statements_analyzer->getFilePath())); $path_to_file = $include_path ?: $path_to_file; - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file); - } - - if ($is_path_relative) { + if (Path::isRelative($path_to_file)) { $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file; } } else { @@ -287,13 +282,7 @@ public static function getPathTo( string $file_name, Config $config, ): ?string { - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $file_name[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $file_name); - } - - if ($is_path_relative) { + if (Path::isRelative($file_name)) { $file_name = $config->base_dir . DIRECTORY_SEPARATOR . $file_name; } diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 837e23c82b9..672a1d168c4 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -317,11 +317,9 @@ static function (string $arg) use ($valid_long_options): void { $path_to_config = CliUtils::getPathToConfig($options); - if (isset($options['tcp'])) { - if (!is_string($options['tcp'])) { - fwrite(STDERR, 'tcp url should be a string' . PHP_EOL); - exit(1); - } + if (isset($options['tcp']) && !is_string($options['tcp'])) { + fwrite(STDERR, 'tcp url should be a string' . PHP_EOL); + exit(1); } $config = CliUtils::initializeConfig( diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index b1b8cc8b4c7..77d122c7e34 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -236,7 +236,7 @@ public static function getArguments(): array } if ($input_path[0] === '-' && strlen($input_path) === 2) { - if ($input_path[1] === 'c' || $input_path[1] === 'f') { + if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') { ++$i; } continue; @@ -275,7 +275,7 @@ public static function getPathsToCheck(string|array|false|null $f_paths): ?array $input_path = $input_paths[$i]; if ($input_path[0] === '-' && strlen($input_path) === 2) { - if ($input_path[1] === 'c' || $input_path[1] === 'f') { + if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') { ++$i; } continue; @@ -291,6 +291,7 @@ public static function getPathsToCheck(string|array|false|null $f_paths): ?array $ignored_arguments = array( 'config', 'printer', + 'root', ); if (in_array(substr($input_path, 2), $ignored_arguments, true)) { diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 2d826dc0891..d088c06eac6 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -218,8 +218,8 @@ public static function resolve( return new TArray([Type::getArrayKey(), Type::getMixed()]); } - foreach ($spread_array->properties as $spread_array_type) { - $properties[$auto_key++] = $spread_array_type; + foreach ($spread_array->properties as $k => $spread_array_type) { + $properties[is_string($k) ? $k : $auto_key++] = $spread_array_type; } continue; } diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php index e672e5109b1..330abb0398d 100644 --- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php @@ -91,7 +91,6 @@ static function ( $start_diff = $b_start - $a_start; $line_diff = $b->getLine() - $a->getLine(); - /** @psalm-suppress MixedArrayAssignment */ $diff_map[] = [$a_start, $a_end, $start_diff, $line_diff]; return true; @@ -185,7 +184,6 @@ static function ( } if (!$signature_change && !$body_change) { - /** @psalm-suppress MixedArrayAssignment */ $diff_map[] = [$a_start, $a_end, $b_start - $a_start, $b->getLine() - $a->getLine()]; } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 8bc21c538bd..8dec8afd667 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -772,11 +772,8 @@ function (IssueData $issue_data): Diagnostic { //Process Baseline $file = $issue_data->file_name; $type = $issue_data->type; - /** @psalm-suppress MixedArrayAccess */ if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) { - /** @psalm-suppress MixedArrayAccess, MixedArgument */ if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) { - /** @psalm-suppress MixedArrayAccess, MixedAssignment */ $position = array_search( str_replace("\r\n", "\n", trim($issue_data->selected_text)), $issue_baseline[$file][$type]['s'], @@ -785,16 +782,12 @@ function (IssueData $issue_data): Diagnostic { if ($position !== false) { $issue_data->severity = IssueData::SEVERITY_INFO; - /** @psalm-suppress MixedArgument */ array_splice($issue_baseline[$file][$type]['s'], $position, 1); - /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ $issue_baseline[$file][$type]['o']--; } } else { - /** @psalm-suppress MixedArrayAssignment */ $issue_baseline[$file][$type]['s'] = []; $issue_data->severity = IssueData::SEVERITY_INFO; - /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ $issue_baseline[$file][$type]['o']--; } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index bf5ac885de6..801f718a2dd 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -22,13 +22,13 @@ use Psalm\Storage\FileStorage; use Psalm\Storage\FunctionLikeStorage; use Psalm\Type; +use Symfony\Component\Filesystem\Path; use function assert; use function defined; use function dirname; use function explode; use function in_array; -use function preg_match; use function strpos; use function strtolower; use function substr; @@ -318,13 +318,7 @@ public static function visitInclude( $include_path = IncludeAnalyzer::resolveIncludePath($path_to_file, dirname($file_storage->file_path)); $path_to_file = $include_path ?: $path_to_file; - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file); - } - - if ($is_path_relative) { + if (Path::isRelative($path_to_file)) { $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file; } } else { diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index 8a39b72c976..d479c926e48 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -51,6 +51,19 @@ public static function isContainedBy( return true; } + if ($container_type_part instanceof TKeyedArray + && $input_type_part instanceof TArray + && !$container_type_part->is_list + && !$container_type_part->isNonEmpty() + && !$container_type_part->isSealed() + && $input_type_part->equals( + $container_type_part->getGenericArrayType($container_type_part->isNonEmpty()), + false, + ) + ) { + return true; + } + if ($container_type_part instanceof TKeyedArray && $input_type_part instanceof TArray ) { diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 0bc11918e7a..c80c79acc51 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -57,6 +57,9 @@ final class TypeCombination /** @var array */ public array $objectlike_entries = []; + /** @var array */ + public array $objectlike_class_string_keys = []; + public bool $objectlike_sealed = true; public ?Union $objectlike_key_type = null; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 67850a2846b..ba98e740a12 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -663,6 +663,7 @@ private static function scrapeTypeProperties( $has_defined_keys = false; + $class_strings = $type->class_strings ?? []; foreach ($type->properties as $candidate_property_name => $candidate_property_type) { $value_type = $combination->objectlike_entries[$candidate_property_name] ?? null; @@ -702,6 +703,19 @@ private static function scrapeTypeProperties( } unset($missing_entries[$candidate_property_name]); + + if (is_int($candidate_property_name)) { + continue; + } + + if (isset($combination->objectlike_class_string_keys[$candidate_property_name])) { + $combination->objectlike_class_string_keys[$candidate_property_name] = + $combination->objectlike_class_string_keys[$candidate_property_name] + && ($class_strings[$candidate_property_name] ?? false); + } else { + $combination->objectlike_class_string_keys[$candidate_property_name] = + ($class_strings[$candidate_property_name] ?? false); + } } if ($type->fallback_params) { @@ -1062,6 +1076,9 @@ private static function scrapeStringProperties( if ($has_only_numeric_strings) { $combination->value_types['string'] = $type; + } elseif (count($combination->strings) === 1 && !$has_only_non_empty_strings) { + $combination->value_types['string'] = $type; + return; } elseif ($has_only_non_empty_strings) { $combination->value_types['string'] = new TNonEmptyString(); } else { @@ -1416,7 +1433,7 @@ private static function handleKeyedArrayEntries( } else { $objectlike = new TKeyedArray( $combination->objectlike_entries, - null, + array_filter($combination->objectlike_class_string_keys), $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 5b3bb664fe7..c51c3a2c372 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -50,13 +50,16 @@ use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TPropertiesOf; +use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateIndexedAccess; use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; @@ -85,6 +88,7 @@ use function defined; use function end; use function explode; +use function filter_var; use function get_class; use function in_array; use function is_int; @@ -98,6 +102,9 @@ use function strtolower; use function strtr; use function substr; +use function trim; + +use const FILTER_VALIDATE_INT; /** * @psalm-suppress InaccessibleProperty Allowed during construction @@ -644,6 +651,46 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TArray($generic_params, $from_docblock); } @@ -672,6 +719,46 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for non-empty-array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); } diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 61d2f0fb53c..c6b251a564d 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -132,6 +132,14 @@ final class IssueBuffer */ public static function accepts(CodeIssue $e, array $suppressed_issues = [], bool $is_fixable = false): bool { + $config = Config::getInstance(); + $project_analyzer = ProjectAnalyzer::getInstance(); + $codebase = $project_analyzer->getCodebase(); + $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); + if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { + return false; + } + if (self::isSuppressed($e, $suppressed_issues)) { return false; } @@ -257,11 +265,6 @@ public static function add(CodeIssue $e, bool $is_fixable = false): bool $project_analyzer = ProjectAnalyzer::getInstance(); $codebase = $project_analyzer->getCodebase(); - $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); - if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { - return false; - } - $fqcn_parts = explode('\\', get_class($e)); $issue_type = array_pop($fqcn_parts); diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 7960ef264b6..589e6bd0711 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -715,6 +715,8 @@ public static function intersectUnionTypes( ?Union $type_1, ?Union $type_2, Codebase $codebase, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Union { if ($type_2 === null && $type_1 === null) { throw new UnexpectedValueException('At least one type must be provided to combine'); @@ -768,6 +770,8 @@ public static function intersectUnionTypes( $type_2_atomic, $codebase, $intersection_performed, + $allow_interface_equality, + $allow_float_int_equality, ); if (null !== $intersection_atomic) { @@ -841,6 +845,8 @@ private static function intersectAtomicTypes( Atomic $type_2_atomic, Codebase $codebase, bool &$intersection_performed, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Atomic { $intersection_atomic = null; $wider_type = null; @@ -886,6 +892,8 @@ private static function intersectAtomicTypes( $codebase, $type_2_atomic, $type_1_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; @@ -894,6 +902,8 @@ private static function intersectAtomicTypes( $codebase, $type_1_atomic, $type_2_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index cf90faf8851..e928740ac5e 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -83,7 +83,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index 028252540de..cacad52c00c 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -207,7 +207,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality)) { + if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index d5f577e8af5..1479233d7b0 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -113,7 +113,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index f8153751a04..a9b9ae988a4 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -111,7 +111,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 80f9f968319..ed6a9c6cfb5 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -659,11 +659,11 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } if ($this->fallback_params !== null && $other_type->fallback_params !== null) { - if (!$this->fallback_params[0]->equals($other_type->fallback_params[0])) { + if (!$this->fallback_params[0]->equals($other_type->fallback_params[0], false, false)) { return false; } - if (!$this->fallback_params[1]->equals($other_type->fallback_params[1])) { + if (!$this->fallback_params[1]->equals($other_type->fallback_params[1], false, false)) { return false; } } @@ -673,7 +673,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 1f54c63c8a8..e43f6efea2b 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -208,7 +208,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 6cc30cdc79f..1836641a169 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -22,6 +22,7 @@ use Psalm\Issue\TypeDoesNotContainType; use Psalm\IssueBuffer; use Psalm\Storage\Assertion; +use Psalm\Storage\Assertion\ArrayKeyDoesNotExist; use Psalm\Storage\Assertion\ArrayKeyExists; use Psalm\Storage\Assertion\Empty_; use Psalm\Storage\Assertion\Falsy; @@ -46,6 +47,9 @@ use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; +use Psalm\Type\Atomic\TList; +use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; @@ -169,6 +173,7 @@ public static function reconcileKeyedTypes( $has_negation = false; $has_isset = false; $has_inverted_isset = false; + $has_inverted_key_exists = false; $has_truthy_or_falsy_or_empty = false; $has_empty = false; $has_count_check = false; @@ -198,7 +203,11 @@ public static function reconcileKeyedTypes( $is_equality = $is_equality && $new_type_part_part instanceof IsIdentical; - $has_inverted_isset = $has_inverted_isset || $new_type_part_part instanceof IsNotIsset; + $has_inverted_isset = $has_inverted_isset + || $new_type_part_part instanceof IsNotIsset; + + $has_inverted_key_exists = $has_inverted_key_exists + || $new_type_part_part instanceof ArrayKeyDoesNotExist; $has_count_check = $has_count_check || $new_type_part_part instanceof NonEmptyCountable; @@ -217,6 +226,7 @@ public static function reconcileKeyedTypes( $code_location, $has_isset, $has_inverted_isset, + $has_inverted_key_exists, $has_empty, $inside_loop, $has_object_array_access, @@ -330,7 +340,12 @@ public static function reconcileKeyedTypes( if ($type_changed || $failed_reconciliation) { $changed_var_ids[$key] = true; - if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_empty && !$is_equality) { + if (substr($key, -1) === ']' + && !$has_inverted_isset + && !$has_inverted_key_exists + && !$has_empty + && !$is_equality + ) { self::adjustTKeyedArrayType( $key_parts, $existing_types, @@ -644,6 +659,7 @@ private static function getValueForKey( ?CodeLocation $code_location, bool $has_isset, bool $has_inverted_isset, + bool $has_inverted_key_exists, bool $has_empty, bool $inside_loop, bool &$has_object_array_access, @@ -717,11 +733,17 @@ private static function getValueForKey( $new_base_type_candidate = $existing_key_type_part->type_params[1]; - if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset) { + if ($new_base_type_candidate->isMixed() + && !$has_isset + && !$has_inverted_isset + && !$has_inverted_key_exists + ) { return $new_base_type_candidate; } - if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { + if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) + && isset($new_assertions[$new_base_key]) + ) { if ($has_inverted_isset && $new_base_key === $key) { $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); @@ -750,7 +772,7 @@ private static function getValueForKey( } elseif ($existing_key_type_part instanceof TString) { $new_base_type_candidate = Type::getString(); } elseif ($existing_key_type_part instanceof TNamedObject - && ($has_isset || $has_inverted_isset) + && ($has_isset || $has_inverted_isset || $has_inverted_key_exists) ) { $has_object_array_access = true; @@ -1103,85 +1125,108 @@ private static function adjustTKeyedArrayType( throw new UnexpectedValueException('Not expecting null array key'); } + $array_key_offsets = []; if ($array_key[0] === '$') { - return; + if (!isset($existing_types[$array_key])) { + return; + } + $t = $existing_types[$array_key]; + foreach ($t->getAtomicTypes() as $lit) { + if ($lit instanceof TLiteralInt || $lit instanceof TLiteralString) { + $array_key_offsets []= $lit->value; + continue; + } + return; + } + } else { + $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' + ? substr($array_key, 1, -1) + : $array_key + ; } - $array_key_offset = $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; - $base_key = implode($key_parts); - if (isset($existing_types[$base_key]) && $array_key_offset !== '') { - foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { - if ($base_atomic_type instanceof TKeyedArray + $result_type = $result_type->setPossiblyUndefined( + $result_type->possibly_undefined || count($array_key_offsets) > 1, + ); + + foreach ($array_key_offsets as $array_key_offset) { + if (isset($existing_types[$base_key]) && $array_key_offset !== false) { + foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { + if ($base_atomic_type instanceof TList) { + $base_atomic_type = $base_atomic_type->getKeyedArray(); + } + if ($base_atomic_type instanceof TKeyedArray || ($base_atomic_type instanceof TArray && !$base_atomic_type->isEmptyArray()) || $base_atomic_type instanceof TClassStringMap - ) { - $new_base_type = $existing_types[$base_key]; + ) { + $new_base_type = $existing_types[$base_key]; - if ($base_atomic_type instanceof TArray) { - $fallback_key_type = $base_atomic_type->type_params[0]; - $fallback_value_type = $base_atomic_type->type_params[1]; + if ($base_atomic_type instanceof TArray) { + $fallback_key_type = $base_atomic_type->type_params[0]; + $fallback_value_type = $base_atomic_type->type_params[1]; - $base_atomic_type = new TKeyedArray( - [ + $base_atomic_type = new TKeyedArray( + [ $array_key_offset => $result_type, - ], - null, - $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], - ); - } elseif ($base_atomic_type instanceof TClassStringMap) { - // do nothing - } else { - $properties = $base_atomic_type->properties; - $properties[$array_key_offset] = $result_type; - if ($base_atomic_type->is_list + ], + null, + $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], + ); + } elseif ($base_atomic_type instanceof TClassStringMap) { + // do nothing + } else { + $properties = $base_atomic_type->properties; + $properties[$array_key_offset] = $result_type; + if ($base_atomic_type->is_list && (!is_numeric($array_key_offset) || ($array_key_offset && !isset($properties[$array_key_offset-1]) ) ) - ) { - if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { - $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( - $result_type->isNever(), - ); - for ($x = 0; $x < $array_key_offset; $x++) { - $properties[$x] ??= $fallback; + ) { + if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { + $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( + $result_type->isNever(), + ); + for ($x = 0; $x < $array_key_offset; $x++) { + $properties[$x] ??= $fallback; + } + ksort($properties); + $base_atomic_type = $base_atomic_type->setProperties($properties); + } else { + // This should actually be a paradox + $base_atomic_type = new TKeyedArray( + $properties, + null, + $base_atomic_type->fallback_params, + false, + $base_atomic_type->from_docblock, + ); } - ksort($properties); - $base_atomic_type = $base_atomic_type->setProperties($properties); } else { - // This should actually be a paradox - $base_atomic_type = new TKeyedArray( - $properties, - null, - $base_atomic_type->fallback_params, - false, - $base_atomic_type->from_docblock, - ); + $base_atomic_type = $base_atomic_type->setProperties($properties); } - } else { - $base_atomic_type = $base_atomic_type->setProperties($properties); } - } - $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); - $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; + $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; - if ($key_parts[count($key_parts) - 1] === ']') { - self::adjustTKeyedArrayType( - $key_parts, - $existing_types, - $changed_var_ids, - $new_base_type, - ); - } + if ($key_parts[count($key_parts) - 1] === ']') { + self::adjustTKeyedArrayType( + $key_parts, + $existing_types, + $changed_var_ids, + $new_base_type, + ); + } - $existing_types[$base_key] = $new_base_type; - break; + $existing_types[$base_key] = $new_base_type; + break; + } } } } diff --git a/stubs/CoreGenericAttributes.phpstub b/stubs/CoreGenericAttributes.phpstub index 7aa6400df06..92abe9542f8 100644 --- a/stubs/CoreGenericAttributes.phpstub +++ b/stubs/CoreGenericAttributes.phpstub @@ -6,6 +6,12 @@ final class AllowDynamicProperties public function __construct() {} } +#[Attribute(Attribute::TARGET_METHOD)] +final class Override +{ + public function __construct() {} +} + #[Attribute(Attribute::TARGET_PARAMETER)] final class SensitiveParameter { diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 24ba0e50539..3b43831e6ff 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -158,46 +158,46 @@ class ArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Count * Returns whether the requested index exists * @link http://php.net/manual/en/arrayobject.offsetexists.php * - * @param TKey $index The index being checked. + * @param TKey $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.0.0 */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** * Returns the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetget.php * - * @param TKey $index The index with the value. + * @param TKey $offset The index with the value. * @return TValue The value at the specified index or false. * * @since 5.0.0 */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** * Sets the value at the specified index to newval * @link http://php.net/manual/en/arrayobject.offsetset.php * - * @param TKey $index The index being set. - * @param TValue $newval The new value for the index. + * @param TKey $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.0.0 */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** * Unsets the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetunset.php * - * @param TKey $index The index being unset. + * @param TKey $offset The index being unset. * @return void * * @since 5.0.0 */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * Appends the value diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 0e3a935bd78..2f719131f78 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -185,30 +185,30 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Serializable, Coun public function __construct($array = array(), $flags = 0) { } /** - * @param TKey $index The offset being checked. + * @param TKey $offset The offset being checked. * @return bool true if the offset exists, otherwise false */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** - * @param TKey $index The offset to get the value from. + * @param TKey $offset The offset to get the value from. * @return TValue|null The value at offset index, null when accessing invalid indexes * @psalm-ignore-nullable-return */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** - * @param TKey $index The index to set for. - * @param TValue $newval The new value to store at the index. + * @param TKey $offset The index to set for. + * @param TValue $value The new value to store at the index. * @return void */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** - * @param TKey $index The offset to unset. + * @param TKey $offset The offset to unset. * @return void */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * @param TValue $value The value to append. diff --git a/stubs/SPL.phpstub b/stubs/SPL.phpstub index 288ceba770a..7a623c933a0 100644 --- a/stubs/SPL.phpstub +++ b/stubs/SPL.phpstub @@ -15,14 +15,14 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa /** * Add/insert a new value at the specified index * - * @param int $index The index where the new value is to be inserted. - * @param TValue $newval The new value for the index. + * @param int $offset The index where the new value is to be inserted. + * @param TValue $value The new value for the index. * @return void * * @link https://php.net/spldoublylinkedlist.add * @since 5.5.0 */ - public function add($index, $newval) {} + public function add($offset, $value) {} /** * Pops a node from the end of the doubly linked list @@ -107,49 +107,49 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa public function isEmpty() {} /** - * Returns whether the requested $index exists + * Returns whether the requested $offset exists * @link https://php.net/manual/en/spldoublylinkedlist.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.3.0 */ - public function offsetExists($index) {} + public function offsetExists($offset) {} /** - * Returns the value at the specified $index + * Returns the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetget.php * - * @param int $index The index with the value. + * @param int $offset The index with the value. * @return TValue The value at the specified index. * * @since 5.3.0 */ - public function offsetGet($index) {} + public function offsetGet($offset) {} /** - * Sets the value at the specified $index to $newval + * Sets the value at the specified $offset to $value * @link https://php.net/manual/en/spldoublylinkedlist.offsetset.php * - * @param int $index The index being set. - * @param TValue $newval The new value for the index. + * @param int $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.3.0 */ - public function offsetSet($index, $newval) {} + public function offsetSet($offset, $value) {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetunset.php * - * @param int $index The index being unset. + * @param int $offset The index being unset. * @return void * * @since 5.3.0 */ - public function offsetUnset($index) {} + public function offsetUnset($offset) {} /** * Return current array entry @@ -297,46 +297,46 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { * Returns whether the specified index exists * @link https://php.net/manual/en/splfixedarray.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, and false otherwise. * * @since 5.3.0 */ - public function offsetExists(int $index): bool {} + public function offsetExists(int $offset): bool {} /** * Sets a new value at a specified index * @link https://php.net/manual/en/splfixedarray.offsetset.php * - * @param int $index The index being sent. - * @param TValue $newval The new value for the index + * @param int $offset The index being sent. + * @param TValue $value The new value for the index * @return void * * @since 5.3.0 */ - public function offsetSet(int $index, $newval): void {} + public function offsetSet(int $offset, $value): void {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/splfixedarray.offsetunset.php * - * @param int $index The index being unset + * @param int $offset The index being unset * @return void * * @since 5.3.0 */ - public function offsetUnset(int $index): void {} + public function offsetUnset(int $offset): void {} /** * Returns the value at the specified index * @link https://php.net/manual/en/splfixedarray.offsetget.php * - * @param int $index The index with the value + * @param int $offset The index with the value * @return TValue The value at the specified index * * @since 5.3.0 */ - public function offsetGet(int $index) {} + public function offsetGet(int $offset) {} } diff --git a/stubs/extensions/dom.phpstub b/stubs/extensions/dom.phpstub index 76563f18f01..866d5dbfa07 100644 --- a/stubs/extensions/dom.phpstub +++ b/stubs/extensions/dom.phpstub @@ -979,8 +979,8 @@ class DOMXPath public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} /** - * @return DOMNodeList|false * @psalm-taint-sink xpath $expression + * @return DOMNodeList|false */ public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index cdd002de207..1c8b15c4969 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1383,7 +1383,15 @@ public function barBar() { }', 'error_message' => 'MissingDocblockType', ], - + 'invalidArrayKeyType' => [ + 'code' => ' $arg + * @return void + */ + function foo($arg) {}', + 'error_message' => 'InvalidDocblock', + ], 'invalidClassMethodReturnBrackets' => [ 'code' => ' [ + 'code' => ' [ + '$result===' => 'array{a: true, b: true}', + '$resultOpt===' => 'array{a?: true, b?: true}', + ], + ], + 'assignUnionOfLiteralsClassKeys' => [ + 'code' => ' $v) { + $vv = new $k; + }', + 'assertions' => [ + '$result===' => 'array{a::class: true, b::class: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' [ '$foo' => 'array{0: string, 1: string, 2: string}', '$bar' => 'list{int, int, int}', - '$bat' => 'non-empty-array', + '$bat' => 'array{a: int, b: int, c: int}', ], ], 'implicitStringArrayCreation' => [ @@ -981,6 +1020,7 @@ function updateArray(array $arr) : array { $a = []; foreach (["one", "two", "three"] as $key) { + $a[$key] ??= 0; $a[$key] += rand(0, 10); } @@ -1260,6 +1300,29 @@ function foo(array $arr) : string { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'constantArraySpreadWithString' => [ + 'code' => ' "a", + "b" => "b", + ]; + } + + class ChildClass extends BaseClass { + public const A = [ + ...parent::KEYS, + "c" => "c", + ]; + } + + $a = ChildClass::A;', + 'assertions' => [ + '$a===' => "array{a: 'a', b: 'b', c: 'c'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'listPropertyAssignmentAfterIsset' => [ 'code' => ' 'non-empty-array>', ], ], + 'stringIntKeys' => [ + 'code' => ' $arg + * @return bool + */ + function foo($arg) { + foreach ($arg as $k => $v) { + if ( $k === 15 ) { + return true; + } + + if ( $k === 17 ) { + return false; + } + } + + return true; + } + + $x = ["15" => "a", 17 => "b"]; + foo($x);', + ], ]; } @@ -2437,7 +2523,8 @@ public function getThisName($offset, $weird_array): string { return $weird_array[$offset]; } }', - 'error_message' => 'InvalidArrayOffset', + 'error_message' => 'MixedArrayAccess', + 'ignored_issues' => ['InvalidDocblock'], ], 'unpackTypedIterableWithStringKeysIntoArray' => [ 'code' => ' [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + ], ]; } @@ -126,6 +153,34 @@ function getKeys() { ', 'error_message' => 'InvalidReturnStatement', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + 'error_message' => 'InvalidReturnStatement', + ], ]; } } diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 072779059b0..a9e6335c15a 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -295,6 +295,22 @@ class Foo 'ignored_issues' => [], 'php_version' => '8.2', ], + 'override' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'sensitiveParameter' => [ 'code' => ' [ - 'code' => ' [ 'code' => ' [ + 'code' => ') $arg + * @return void + */ + function foo($arg) {} + + /** + * @param array{a?: string}&array $cb_arg + * @return void + */ + function bar($cb_arg) {} + + foo("bar");', + ], ]; } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 61ae873cd96..7e3b5b77fde 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -19,28 +19,63 @@ public function providerValidCodeParse(): iterable return [ 'byRefUseVar' => [ 'code' => ' [ + '$testBefore===' => '123', + '$testInsideBefore===' => "'test'|123|null", + '$testInsideAfter===' => "'test'|null", + '$test===' => "'test'|123", + + '$doNotContaminate===' => '123', + ], + ], + 'byRefUseSelf' => [ + 'code' => ' [ + 'code' => ' [ 'code' => ' 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but parent type A provided', ], - 'closureByRefUseToMixed' => [ - 'code' => ' 'MixedReturnStatement', - ], 'noCrashWhenComparingIllegitimateCallable' => [ 'code' => 'analyzeFile('somefile.php', new Context); self::assertSame(0, IssueBuffer::getErrorCount()); } + /** + * @test + */ + public function addingCodeIssueIsMarkedAsRedundant(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessage('UnusedPsalmSuppress'); + + $this->addFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + 'getIssue(); + if ($issue->code_location->file_path !== (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php') { + return null; + } + if ($issue instanceof InvalidReturnStatement && $event->isFixable() === false) { + return false; + } elseif ($issue instanceof InvalidReturnType && $event->isFixable() === true) { + return false; + } + return null; + } + }; + + (new PluginRegistrationSocket($this->codebase->config, $this->codebase)) + ->registerHooksFromClass(get_class($eventHandler)); + + $this->analyzeFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + new Context, + ); + } } diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 34a5b06f976..6757b43088f 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -19,6 +19,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\VersionUtils; use Psalm\Issue\TooManyArguments; use Psalm\Issue\UndefinedFunction; use Psalm\Tests\Config\Plugin\FileTypeSelfRegisteringPlugin; @@ -60,11 +61,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 93f2eb95bb3..6127135342b 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -14,6 +14,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\AfterEveryFunctionCallAnalysisInterface; @@ -48,11 +49,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index a9599e5e9e9..eb28397f3ad 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -2360,6 +2360,17 @@ function fooFoo(int $a): void {} fooFoo("string");', 'error_message' => 'InvalidArgument', ], + 'invalidArgumentFalseTrueExpected' => [ + 'code' => ' 'InvalidArgument', + ], 'builtinFunctioninvalidArgumentWithWeakTypes' => [ 'code' => ' [ 'code' => '|array> + * @return key-of|array> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asString) { + if ($asString) { + return "42"; } return 42; } @@ -194,14 +194,14 @@ public function getKey() { ', 'error_message' => 'InvalidReturnStatement', ], - 'noStringAllowedInKeyOfIntFloatArray' => [ + 'noStringAllowedInKeyOfIntFloatStringArray' => [ 'code' => '|array> + * @return key-of|array<"42.0", string>> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asInt) { + if ($asInt) { + return 42; } return "42"; } diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index 125be719e4d..cd6d00358d5 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1029,9 +1029,7 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - if (!isset($arr[$i]["a"])) { - $arr[$i]["a"] = 0; - } + $arr[$i]["a"] ??= 0; $arr[$i]["a"] += 5; } diff --git a/tests/MatchTest.php b/tests/MatchTest.php index ccab5ffef3a..43c1e0c1492 100644 --- a/tests/MatchTest.php +++ b/tests/MatchTest.php @@ -169,6 +169,21 @@ function process(Obj1|Obj2 $obj): int|string 'ignored_issues' => [], 'php_version' => '8.0', ], + 'nullCoalesce' => [ + 'code' => <<<'PHP' + null, + true => 1, + } ?? 2; + PHP, + 'assertions' => [ + '$match' => 'int', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } diff --git a/tests/ProjectCheckerTest.php b/tests/ProjectCheckerTest.php index c2b07415b91..4d51353d503 100644 --- a/tests/ProjectCheckerTest.php +++ b/tests/ProjectCheckerTest.php @@ -10,6 +10,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; @@ -45,11 +46,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index 5ae7048cabe..bf575406a98 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -448,6 +448,81 @@ function gimmeFoo(): MyEnum return MyEnum::FOO(); }', ], + 'pureThroughCallStaticInTrait' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => 'getString());', ], - 'makeByRefUseMixed' => [ - 'code' => ' [], - 'ignored_issues' => ['MixedArgument'], - ], 'assignByRefToMixed' => [ 'code' => ' [ 'name' => 'Psalm', 'informationUri' => 'https://psalm.dev', - 'version' => '4.0.0', + 'version' => VersionUtils::getPsalmVersion(), 'rules' => [ [ 'id' => '246', diff --git a/tests/StubTest.php b/tests/StubTest.php index 5b9aa0eb095..eeedb64658d 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -15,6 +15,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use function assert; @@ -40,11 +41,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index cf73c4b9e6b..0643ef22f22 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -14,6 +14,7 @@ use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Type\TypeParser; use Psalm\Internal\Type\TypeTokenizer; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use Psalm\Type\Union; @@ -58,11 +59,11 @@ public static function setUpBeforeClass(): void ini_set('memory_limit', '-1'); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } parent::setUpBeforeClass(); diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index f53f15300a4..cb03d9f1138 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -90,6 +90,38 @@ function expectsTraversableOrArray($_a): void } ', ], + 'emptyStringNumericStringDontCombine' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' */ + } + ', + ], + 'arrayKeyExistsNoSideEffects' => [ + 'code' => ' */ + } + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => ' [], 'ignored_issues' => ['MixedArrayAccess'], ], + 'issetWithArrayAssignment' => [ + 'code'=> ' [ + 'code'=> ' [ + 'code'=> ' [ 'code' => ' [ - 'code' => ' [ 'code' => ' [ @@ -2235,9 +2233,6 @@ function string_to_float(string $a): float { ], 'allowUseByRef' => [ 'code' => '