var, $statements_analyzer->getFQCLN(), $statements_analyzer ); if ($stmt->dim && ExpressionAnalyzer::analyze($statements_analyzer, $stmt->dim, $context) === false) { return false; } $keyed_array_var_id = ExpressionIdentifier::getArrayVarId( $stmt, $statements_analyzer->getFQCLN(), $statements_analyzer ); $dim_var_id = null; $new_offset_type = null; if ($stmt->dim) { $used_key_type = $statements_analyzer->node_data->getType($stmt->dim) ?: Type::getMixed(); $dim_var_id = ExpressionIdentifier::getArrayVarId( $stmt->dim, $statements_analyzer->getFQCLN(), $statements_analyzer ); } else { $used_key_type = Type::getInt(); } if (ExpressionAnalyzer::analyze( $statements_analyzer, $stmt->var, $context ) === false) { return false; } $stmt_var_type = $statements_analyzer->node_data->getType($stmt->var); $codebase = $statements_analyzer->getCodebase(); if ($keyed_array_var_id && $context->hasVariable($keyed_array_var_id) && !$context->vars_in_scope[$keyed_array_var_id]->possibly_undefined && $stmt_var_type && !$stmt_var_type->hasClassStringMap() ) { $stmt_type = clone $context->vars_in_scope[$keyed_array_var_id]; $statements_analyzer->node_data->setType( $stmt, $stmt_type ); if ($array_var_id) { self::taintArrayType( $statements_analyzer, $codebase, $stmt, $stmt_type, $array_var_id ); } return true; } $can_store_result = false; if ($stmt_var_type) { if ($stmt_var_type->isNull()) { if (!$context->inside_isset) { if (IssueBuffer::accepts( new NullArrayAccess( 'Cannot access array value on null variable ' . $array_var_id, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if ($stmt_type = $statements_analyzer->node_data->getType($stmt)) { $statements_analyzer->node_data->setType( $stmt, Type::combineUnionTypes($stmt_type, Type::getNull()) ); } else { $statements_analyzer->node_data->setType($stmt, Type::getNull()); } return true; } $stmt_type = self::getArrayAccessTypeGivenOffset( $statements_analyzer, $stmt, $stmt_var_type, $used_key_type, false, $array_var_id, $context, null ); if ($stmt->dim && $stmt_var_type->hasArray()) { /** * @psalm-suppress PossiblyUndefinedStringArrayOffset * @var TArray|ObjectLike|TList|Type\Atomic\TClassStringMap */ $array_type = $stmt_var_type->getAtomicTypes()['array']; if ($array_type instanceof Type\Atomic\TClassStringMap) { $array_value_type = Type::getMixed(); } elseif ($array_type instanceof TArray) { $array_value_type = $array_type->type_params[1]; } elseif ($array_type instanceof TList) { $array_value_type = $array_type->type_param; } else { $array_value_type = $array_type->getGenericValueType(); } if ($context->inside_assignment || !$array_value_type->isMixed()) { $can_store_result = true; } } $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($array_var_id) { self::taintArrayType( $statements_analyzer, $codebase, $stmt, $stmt_type, $array_var_id ); } if ($context->inside_isset && $stmt->dim && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim)) && $stmt_var_type->hasArray() && ($stmt->var instanceof PhpParser\Node\Expr\ClassConstFetch || $stmt->var instanceof PhpParser\Node\Expr\ConstFetch) ) { /** * @psalm-suppress PossiblyUndefinedStringArrayOffset * @var TArray|ObjectLike|TList */ $array_type = $stmt_var_type->getAtomicTypes()['array']; if ($array_type instanceof TArray) { $const_array_key_type = $array_type->type_params[0]; } elseif ($array_type instanceof TList) { $const_array_key_type = Type::getInt(); } else { $const_array_key_type = $array_type->getGenericKeyType(); } if ($dim_var_id && !$const_array_key_type->hasMixed() && !$stmt_dim_type->hasMixed() ) { $new_offset_type = clone $stmt_dim_type; $const_array_key_atomic_types = $const_array_key_type->getAtomicTypes(); foreach ($new_offset_type->getAtomicTypes() as $offset_key => $offset_atomic_type) { if ($offset_atomic_type instanceof TString || $offset_atomic_type instanceof TInt ) { if (!isset($const_array_key_atomic_types[$offset_key]) && !TypeAnalyzer::isContainedBy( $codebase, new Type\Union([$offset_atomic_type]), $const_array_key_type ) ) { $new_offset_type->removeType($offset_key); } } elseif (!TypeAnalyzer::isContainedBy( $codebase, $const_array_key_type, new Type\Union([$offset_atomic_type]) )) { $new_offset_type->removeType($offset_key); } } } } } if ($keyed_array_var_id && $context->hasVariable($keyed_array_var_id, $statements_analyzer) && (!($stmt_type = $statements_analyzer->node_data->getType($stmt)) || $stmt_type->isVanillaMixed()) ) { $statements_analyzer->node_data->setType($stmt, $context->vars_in_scope[$keyed_array_var_id]); } if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))) { $stmt_type = Type::getMixed(); $statements_analyzer->node_data->setType($stmt, $stmt_type); } else { if ($stmt_type->possibly_undefined && !$context->inside_isset && !$context->inside_unset && ($stmt_var_type && !$stmt_var_type->hasMixed()) ) { if (IssueBuffer::accepts( new PossiblyUndefinedArrayOffset( 'Possibly undefined array key ' . $keyed_array_var_id, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } $stmt_type->possibly_undefined = false; } if ($context->inside_isset && $dim_var_id && $new_offset_type && $new_offset_type->getAtomicTypes()) { $context->vars_in_scope[$dim_var_id] = $new_offset_type; } if ($keyed_array_var_id && !$context->inside_isset && $can_store_result) { $context->vars_in_scope[$keyed_array_var_id] = $stmt_type; $context->vars_possibly_in_scope[$keyed_array_var_id] = true; // reference the variable too $context->hasVariable($keyed_array_var_id, $statements_analyzer); } if ($codebase->taint && ($stmt_var_type = $statements_analyzer->node_data->getType($stmt->var))) { $sources = []; $sources = \array_merge($sources, $stmt_var_type->parent_nodes ?: []); if ($sources) { $stmt_type->parent_nodes = $sources; } } return true; } /** * @param Type\Union $array_type * @param Type\Union $offset_type * @param bool $in_assignment * @param null|string $array_var_id * * @return Type\Union */ public static function getArrayAccessTypeGivenOffset( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\ArrayDimFetch $stmt, Type\Union $array_type, Type\Union $offset_type, bool $in_assignment, ?string $array_var_id, Context $context, PhpParser\Node\Expr $assign_value = null, Type\Union $replacement_type = null ) { $codebase = $statements_analyzer->getCodebase(); $has_array_access = false; $non_array_types = []; $has_valid_offset = false; $expected_offset_types = []; $key_value = null; if ($stmt->dim instanceof PhpParser\Node\Scalar\String_ || $stmt->dim instanceof PhpParser\Node\Scalar\LNumber ) { $key_value = $stmt->dim->value; } elseif ($stmt->dim && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))) { foreach ($stmt_dim_type->getAtomicTypes() as $possible_value_type) { if ($possible_value_type instanceof TLiteralString || $possible_value_type instanceof TLiteralInt ) { if ($key_value !== null) { $key_value = null; break; } $key_value = $possible_value_type->value; } elseif ($possible_value_type instanceof TString || $possible_value_type instanceof TInt ) { $key_value = null; break; } } } $array_access_type = null; if ($offset_type->isNull()) { if (IssueBuffer::accepts( new NullArrayOffset( 'Cannot access value on variable ' . $array_var_id . ' using null offset', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } if ($in_assignment) { $offset_type->removeType('null'); $offset_type->addType(new TLiteralInt(0)); } } if ($offset_type->isNullable() && !$context->inside_isset) { if (!$offset_type->ignore_nullable_issues) { if (IssueBuffer::accepts( new PossiblyNullArrayOffset( 'Cannot access value on variable ' . $array_var_id . ' using possibly null offset ' . $offset_type, new CodeLocation($statements_analyzer->getSource(), $stmt->var) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if ($in_assignment) { $offset_type->removeType('null'); if (!$offset_type->ignore_nullable_issues) { $offset_type->addType(new TLiteralInt(0)); } } } foreach ($array_type->getAtomicTypes() as $type_string => $type) { $original_type = $type; if ($type instanceof TMixed || $type instanceof TTemplateParam || $type instanceof TEmpty) { if (!$type instanceof TTemplateParam || $type->as->isMixed() || !$type->as->isSingle()) { if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); } if (!$context->inside_isset) { if ($in_assignment) { if (IssueBuffer::accepts( new MixedArrayAssignment( 'Cannot access array value on mixed variable ' . $array_var_id, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new MixedArrayAccess( 'Cannot access array value on mixed variable ' . $array_var_id, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } $has_valid_offset = true; if (!$array_access_type) { $array_access_type = Type::getMixed( $type instanceof TEmpty ); } else { $array_access_type = Type::combineUnionTypes( $array_access_type, Type::getMixed($type instanceof TEmpty) ); } continue; } $type = clone array_values($type->as->getAtomicTypes())[0]; } if ($type instanceof TNull) { if ($array_type->ignore_nullable_issues) { continue; } if ($in_assignment) { if ($replacement_type) { if ($array_access_type) { $array_access_type = Type::combineUnionTypes($array_access_type, $replacement_type); } else { $array_access_type = clone $replacement_type; } } else { if (IssueBuffer::accepts( new PossiblyNullArrayAssignment( 'Cannot access array value on possibly null variable ' . $array_var_id . ' of type ' . $array_type, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } $array_access_type = new Type\Union([new TEmpty]); } } else { if (!$context->inside_isset) { if (IssueBuffer::accepts( new PossiblyNullArrayAccess( 'Cannot access array value on possibly null variable ' . $array_var_id . ' of type ' . $array_type, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if ($array_access_type) { $array_access_type = Type::combineUnionTypes($array_access_type, Type::getNull()); } else { $array_access_type = Type::getNull(); } } continue; } if ($type instanceof TArray || $type instanceof ObjectLike || $type instanceof TList || $type instanceof TClassStringMap ) { $has_array_access = true; if ($in_assignment && $type instanceof TArray && (($type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty()) || ($type->type_params[1]->hasMixed() && \is_string($key_value))) ) { $from_empty_array = $type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty(); if ($key_value !== null) { $from_mixed_array = $type->type_params[1]->isMixed(); $previous_key_type = $type->type_params[0]; $previous_value_type = $type->type_params[1]; // ok, type becomes an ObjectLike $array_type->removeType($type_string); $type = new ObjectLike([$key_value => $from_mixed_array ? Type::getMixed() : Type::getEmpty()]); $type->sealed = $from_empty_array; if (!$from_empty_array) { $type->previous_value_type = clone $previous_value_type; $type->previous_key_type = clone $previous_key_type; } $array_type->addType($type); } elseif (!$stmt->dim && $from_empty_array && $replacement_type) { $array_type->removeType($type_string); $array_type->addType(new Type\Atomic\TNonEmptyList($replacement_type)); continue; } } elseif ($in_assignment && $type instanceof ObjectLike && $type->previous_value_type && $type->previous_value_type->isMixed() && $key_value !== null ) { $type->properties[$key_value] = Type::getMixed(); } $offset_type = self::replaceOffsetTypeWithInts($offset_type); if ($type instanceof TList && (($in_assignment && $stmt->dim) || $original_type instanceof TTemplateParam || !$offset_type->isInt()) ) { $type = new TArray([Type::getInt(), $type->type_param]); } if ($type instanceof TArray) { // if we're assigning to an empty array with a key offset, refashion that array if ($in_assignment) { if ($type->type_params[0]->isEmpty()) { $type->type_params[0] = $offset_type->isMixed() ? Type::getArrayKey() : $offset_type; } } elseif (!$type->type_params[0]->isEmpty()) { $expected_offset_type = $type->type_params[0]->hasMixed() ? new Type\Union([ new TArrayKey ]) : $type->type_params[0]; $templated_offset_type = null; foreach ($offset_type->getAtomicTypes() as $offset_atomic_type) { if ($offset_atomic_type instanceof TTemplateParam) { $templated_offset_type = $offset_atomic_type; } } $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult(); if ($original_type instanceof TTemplateParam && $templated_offset_type) { foreach ($templated_offset_type->as->getAtomicTypes() as $offset_as) { if ($offset_as instanceof Type\Atomic\TTemplateKeyOf && $offset_as->param_name === $original_type->param_name && $offset_as->defining_class === $original_type->defining_class ) { /** @psalm-suppress PropertyTypeCoercion */ $type->type_params[1] = new Type\Union([ new Type\Atomic\TTemplateIndexedAccess( $offset_as->param_name, $templated_offset_type->param_name, $offset_as->defining_class ) ]); $has_valid_offset = true; } } } else { $offset_type_contained_by_expected = TypeAnalyzer::isContainedBy( $codebase, $offset_type, $expected_offset_type, true, $offset_type->ignore_falsable_issues, $union_comparison_results ); if ($codebase->config->ensure_array_string_offsets_exist && $offset_type_contained_by_expected ) { self::checkLiteralStringArrayOffset( $offset_type, $expected_offset_type, $array_var_id, $stmt, $context, $statements_analyzer ); } if ($codebase->config->ensure_array_int_offsets_exist && $offset_type_contained_by_expected ) { self::checkLiteralIntArrayOffset( $offset_type, $expected_offset_type, $array_var_id, $stmt, $context, $statements_analyzer ); } if ((!$offset_type_contained_by_expected && !$union_comparison_results->type_coerced_from_scalar) || $union_comparison_results->to_string_cast ) { if ($union_comparison_results->type_coerced_from_mixed && !$offset_type->isMixed() ) { if (IssueBuffer::accepts( new MixedArrayTypeCoercion( 'Coercion from array offset type \'' . $offset_type->getId() . '\' ' . 'to the expected type \'' . $expected_offset_type->getId() . '\'', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { $expected_offset_types[] = $expected_offset_type->getId(); } if (TypeAnalyzer::canExpressionTypesBeIdentical( $codebase, $offset_type, $expected_offset_type )) { $has_valid_offset = true; } } else { $has_valid_offset = true; } } } if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) { $type->count++; } if ($in_assignment && $replacement_type) { /** @psalm-suppress PropertyTypeCoercion */ $type->type_params[1] = Type::combineUnionTypes( $type->type_params[1], $replacement_type, $codebase ); } if (!$array_access_type) { $array_access_type = $type->type_params[1]; } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $type->type_params[1] ); } if ($array_access_type->isEmpty() && !$array_type->hasMixed() && !$in_assignment && !$context->inside_isset ) { if (IssueBuffer::accepts( new EmptyArrayAccess( 'Cannot access value on empty array variable ' . $array_var_id, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { return Type::getMixed(true); } if (!IssueBuffer::isRecording()) { $array_access_type = Type::getMixed(true); } } } elseif ($type instanceof TList) { // if we're assigning to an empty array with a key offset, refashion that array if (!$in_assignment) { if (!$type instanceof TNonEmptyList || ($key_value > 0 && $key_value > ($type->count - 1)) ) { $expected_offset_type = Type::getInt(); if ($codebase->config->ensure_array_int_offsets_exist) { self::checkLiteralIntArrayOffset( $offset_type, $expected_offset_type, $array_var_id, $stmt, $context, $statements_analyzer ); } } $has_valid_offset = true; } if ($in_assignment && $type instanceof Type\Atomic\TNonEmptyList && $type->count !== null) { $type->count++; } if ($in_assignment && $replacement_type) { $type->type_param = Type::combineUnionTypes( $type->type_param, $replacement_type, $codebase ); } if (!$array_access_type) { $array_access_type = $type->type_param; } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $type->type_param ); } } elseif ($type instanceof TClassStringMap) { $offset_type_parts = array_values($offset_type->getAtomicTypes()); foreach ($offset_type_parts as $offset_type_part) { if ($offset_type_part instanceof Type\Atomic\TClassString) { if ($offset_type_part instanceof Type\Atomic\TTemplateParamClass) { $template_result_get = new TemplateResult( [], [ $type->param_name => [ 'class-string-map' => [ new Type\Union([ new TTemplateParam( $offset_type_part->param_name, $offset_type_part->as_type ? new Type\Union([$offset_type_part->as_type]) : Type::getObject(), $offset_type_part->defining_class ) ]) ] ] ] ); $template_result_set = new TemplateResult( [], [ $offset_type_part->param_name => [ $offset_type_part->defining_class => [ new Type\Union([ new TTemplateParam( $type->param_name, $type->as_type ? new Type\Union([$type->as_type]) : Type::getObject(), 'class-string-map' ) ]) ] ] ] ); } else { $template_result_get = new TemplateResult( [], [ $type->param_name => [ 'class-string-map' => [ new Type\Union([ $offset_type_part->as_type ?: new Type\Atomic\TObject() ]) ] ] ] ); $template_result_set = new TemplateResult( [], [] ); } $expected_value_param_get = clone $type->value_param; $expected_value_param_get->replaceTemplateTypesWithArgTypes( $template_result_get, $codebase ); if ($replacement_type) { $expected_value_param_set = clone $type->value_param; $replacement_type->replaceTemplateTypesWithArgTypes( $template_result_set, $codebase ); $type->value_param = Type::combineUnionTypes( $replacement_type, $expected_value_param_set, $codebase ); } if (!$array_access_type) { $array_access_type = $expected_value_param_get; } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $expected_value_param_get, $codebase ); } } } } else { $generic_key_type = $type->getGenericKeyType(); if (!$stmt->dim && $type->sealed && $type->is_list) { $key_value = count($type->properties); } if ($key_value !== null) { if (isset($type->properties[$key_value]) || $replacement_type) { $has_valid_offset = true; if ($replacement_type) { if (isset($type->properties[$key_value])) { $type->properties[$key_value] = Type::combineUnionTypes( $type->properties[$key_value], $replacement_type ); } else { $type->properties[$key_value] = $replacement_type; } } if (!$array_access_type) { $array_access_type = clone $type->properties[$key_value]; } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $type->properties[$key_value] ); } } elseif ($in_assignment) { $type->properties[$key_value] = new Type\Union([new TEmpty]); if (!$array_access_type) { $array_access_type = clone $type->properties[$key_value]; } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $type->properties[$key_value] ); } } elseif ($type->previous_value_type) { if ($codebase->config->ensure_array_string_offsets_exist) { self::checkLiteralStringArrayOffset( $offset_type, $type->getGenericKeyType(), $array_var_id, $stmt, $context, $statements_analyzer ); } if ($codebase->config->ensure_array_int_offsets_exist) { self::checkLiteralIntArrayOffset( $offset_type, $type->getGenericKeyType(), $array_var_id, $stmt, $context, $statements_analyzer ); } $type->properties[$key_value] = clone $type->previous_value_type; $array_access_type = clone $type->previous_value_type; } elseif ($array_type->hasMixed()) { $has_valid_offset = true; $array_access_type = Type::getMixed(); } else { if ($type->sealed || !$context->inside_isset) { $object_like_keys = array_keys($type->properties); if (count($object_like_keys) === 1) { $expected_keys_string = '\'' . $object_like_keys[0] . '\''; } else { $last_key = array_pop($object_like_keys); $expected_keys_string = '\'' . implode('\', \'', $object_like_keys) . '\' or \'' . $last_key . '\''; } $expected_offset_types[] = $expected_keys_string; } $array_access_type = Type::getMixed(); } } else { $key_type = $generic_key_type->hasMixed() ? Type::getArrayKey() : $generic_key_type; $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult(); $is_contained = TypeAnalyzer::isContainedBy( $codebase, $offset_type, $key_type, true, $offset_type->ignore_falsable_issues, $union_comparison_results ); if ($context->inside_isset && !$is_contained) { $is_contained = TypeAnalyzer::isContainedBy( $codebase, $key_type, $offset_type, true, $offset_type->ignore_falsable_issues ) || TypeAnalyzer::canBeContainedBy( $codebase, $offset_type, $key_type, true, $offset_type->ignore_falsable_issues ); } if (($is_contained || $union_comparison_results->type_coerced_from_scalar || $union_comparison_results->type_coerced_from_mixed || $in_assignment) && !$union_comparison_results->to_string_cast ) { if ($replacement_type) { $generic_params = Type::combineUnionTypes( $type->getGenericValueType(), $replacement_type ); $new_key_type = Type::combineUnionTypes( $generic_key_type, $offset_type->isMixed() ? Type::getArrayKey() : $offset_type ); $property_count = $type->sealed ? count($type->properties) : null; if (!$stmt->dim && $property_count) { ++$property_count; $array_type->removeType($type_string); $type = new TNonEmptyArray([ $new_key_type, $generic_params, ]); $array_type->addType($type); $type->count = $property_count; } else { $array_type->removeType($type_string); $type = new TArray([ $new_key_type, $generic_params, ]); $array_type->addType($type); } if (!$array_access_type) { $array_access_type = clone $generic_params; } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $generic_params ); } } else { if (!$array_access_type) { $array_access_type = $type->getGenericValueType(); } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $type->getGenericValueType() ); } } $has_valid_offset = true; } else { if (!$context->inside_isset || ($type->sealed && !$union_comparison_results->type_coerced) ) { $expected_offset_types[] = $generic_key_type->getId(); } $array_access_type = Type::getMixed(); } } } continue; } if ($type instanceof TString) { if ($in_assignment && $replacement_type) { if ($replacement_type->hasMixed()) { if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); } if (IssueBuffer::accepts( new MixedStringOffsetAssignment( 'Right-hand-side of string offset assignment cannot be mixed', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); } } } if ($type instanceof TSingleLetter) { $valid_offset_type = Type::getInt(false, 0); } elseif ($type instanceof TLiteralString) { if (!strlen($type->value)) { $valid_offset_type = Type::getEmpty(); } elseif (strlen($type->value) < 10) { $valid_offsets = []; for ($i = -strlen($type->value), $l = strlen($type->value); $i < $l; $i++) { $valid_offsets[] = new TLiteralInt($i); } if (!$valid_offsets) { throw new \UnexpectedValueException('This is weird'); } $valid_offset_type = new Type\Union($valid_offsets); } else { $valid_offset_type = Type::getInt(); } } else { $valid_offset_type = Type::getInt(); } if (!TypeAnalyzer::isContainedBy( $codebase, $offset_type, $valid_offset_type, true )) { $expected_offset_types[] = $valid_offset_type->getId(); $array_access_type = Type::getMixed(); } else { $has_valid_offset = true; if (!$array_access_type) { $array_access_type = Type::getSingleLetter(); } else { $array_access_type = Type::combineUnionTypes( $array_access_type, Type::getSingleLetter() ); } } continue; } if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); } if ($type instanceof Type\Atomic\TFalse && $array_type->ignore_falsable_issues) { continue; } if ($type instanceof TNamedObject) { if (strtolower($type->value) === 'simplexmlelement') { $call_array_access_type = new Type\Union([new TNamedObject('SimpleXMLElement')]); } elseif (strtolower($type->value) === 'domnodelist' && $stmt->dim) { $old_data_provider = $statements_analyzer->node_data; $statements_analyzer->node_data = clone $statements_analyzer->node_data; $fake_method_call = new PhpParser\Node\Expr\MethodCall( $stmt->var, new PhpParser\Node\Identifier('item', $stmt->var->getAttributes()), [ new PhpParser\Node\Arg($stmt->dim) ] ); $suppressed_issues = $statements_analyzer->getSuppressedIssues(); if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']); } if (!in_array('MixedMethodCall', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['MixedMethodCall']); } \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( $statements_analyzer, $fake_method_call, $context ); if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']); } if (!in_array('MixedMethodCall', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['MixedMethodCall']); } $call_array_access_type = $statements_analyzer->node_data->getType( $fake_method_call ) ?: Type::getMixed(); $statements_analyzer->node_data = $old_data_provider; } else { $suppressed_issues = $statements_analyzer->getSuppressedIssues(); if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']); } if (!in_array('MixedMethodCall', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['MixedMethodCall']); } if ($in_assignment) { $old_node_data = $statements_analyzer->node_data; $statements_analyzer->node_data = clone $statements_analyzer->node_data; $fake_set_method_call = new PhpParser\Node\Expr\MethodCall( $stmt->var, new PhpParser\Node\Identifier('offsetSet', $stmt->var->getAttributes()), [ new PhpParser\Node\Arg( $stmt->dim ? $stmt->dim : new PhpParser\Node\Expr\ConstFetch( new PhpParser\Node\Name('null'), $stmt->var->getAttributes() ) ), new PhpParser\Node\Arg( $assign_value ?: new PhpParser\Node\Expr\ConstFetch( new PhpParser\Node\Name('null'), $stmt->var->getAttributes() ) ), ] ); \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( $statements_analyzer, $fake_set_method_call, $context ); $statements_analyzer->node_data = $old_node_data; } if ($stmt->dim) { $old_node_data = $statements_analyzer->node_data; $statements_analyzer->node_data = clone $statements_analyzer->node_data; $fake_get_method_call = new PhpParser\Node\Expr\MethodCall( $stmt->var, new PhpParser\Node\Identifier('offsetGet', $stmt->var->getAttributes()), [ new PhpParser\Node\Arg( $stmt->dim ) ] ); \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( $statements_analyzer, $fake_get_method_call, $context ); $call_array_access_type = $statements_analyzer->node_data->getType($fake_get_method_call) ?: Type::getMixed(); $statements_analyzer->node_data = $old_node_data; } else { $call_array_access_type = Type::getVoid(); } $has_array_access = true; if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']); } if (!in_array('MixedMethodCall', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['MixedMethodCall']); } } if (!$array_access_type) { $array_access_type = $call_array_access_type; } else { $array_access_type = Type::combineUnionTypes( $array_access_type, $call_array_access_type ); } } elseif (!$array_type->hasMixed()) { $non_array_types[] = (string)$type; } } if ($non_array_types) { if ($has_array_access) { if ($in_assignment) { if (IssueBuffer::accepts( new PossiblyInvalidArrayAssignment( 'Cannot access array value on non-array variable ' . $array_var_id . ' of type ' . $non_array_types[0], new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ) ) { // do nothing } } elseif (!$context->inside_isset) { if (IssueBuffer::accepts( new PossiblyInvalidArrayAccess( 'Cannot access array value on non-array variable ' . $array_var_id . ' of type ' . $non_array_types[0], new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ) ) { // do nothing } } } else { if ($in_assignment) { if (IssueBuffer::accepts( new InvalidArrayAssignment( 'Cannot access array value on non-array variable ' . $array_var_id . ' of type ' . $non_array_types[0], new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new InvalidArrayAccess( 'Cannot access array value on non-array variable ' . $array_var_id . ' of type ' . $non_array_types[0], new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } $array_access_type = Type::getMixed(); } } if ($offset_type->hasMixed()) { if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); } if (IssueBuffer::accepts( new MixedArrayOffset( 'Cannot access value on variable ' . $array_var_id . ' using mixed offset', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); } if ($expected_offset_types) { $invalid_offset_type = $expected_offset_types[0]; $used_offset = 'using a ' . $offset_type->getId() . ' offset'; if ($key_value !== null) { $used_offset = 'using offset value of ' . (is_int($key_value) ? $key_value : '\'' . $key_value . '\''); } if ($has_valid_offset && $context->inside_isset) { // do nothing } elseif ($has_valid_offset) { if (!$context->inside_unset) { if (IssueBuffer::accepts( new PossiblyInvalidArrayOffset( 'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset . ', expecting ' . $invalid_offset_type, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } else { if (IssueBuffer::accepts( new InvalidArrayOffset( 'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset . ', expecting ' . $invalid_offset_type, new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } } if ($array_access_type === null) { // shouldn’t happen, but don’t crash return Type::getMixed(); } if ($in_assignment) { $array_type->bustCache(); } return $array_access_type; } private static function checkLiteralIntArrayOffset( Type\Union $offset_type, Type\Union $expected_offset_type, ?string $array_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, Context $context, StatementsAnalyzer $statements_analyzer ) : void { if ($context->inside_isset || $context->inside_unset) { return; } if ($offset_type->hasLiteralInt()) { $found_match = false; foreach ($offset_type->getAtomicTypes() as $offset_type_part) { if ($array_var_id && $offset_type_part instanceof TLiteralInt && isset( $context->vars_in_scope[ $array_var_id . '[' . $offset_type_part->value . ']' ] ) && !$context->vars_in_scope[ $array_var_id . '[' . $offset_type_part->value . ']' ]->possibly_undefined ) { $found_match = true; } } if (!$found_match) { if (IssueBuffer::accepts( new PossiblyUndefinedIntArrayOffset( 'Possibly undefined array offset \'' . $offset_type->getId() . '\' ' . 'is risky given expected type \'' . $expected_offset_type->getId() . '\'.' . ' Consider using isset beforehand.', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } } private static function checkLiteralStringArrayOffset( Type\Union $offset_type, Type\Union $expected_offset_type, ?string $array_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, Context $context, StatementsAnalyzer $statements_analyzer ) : void { if ($context->inside_isset || $context->inside_unset) { return; } if ($offset_type->hasLiteralString() && !$expected_offset_type->hasLiteralClassString()) { $found_match = false; foreach ($offset_type->getAtomicTypes() as $offset_type_part) { if ($array_var_id && $offset_type_part instanceof TLiteralString && isset( $context->vars_in_scope[ $array_var_id . '[\'' . $offset_type_part->value . '\']' ] ) && !$context->vars_in_scope[ $array_var_id . '[\'' . $offset_type_part->value . '\']' ]->possibly_undefined ) { $found_match = true; } } if (!$found_match) { if (IssueBuffer::accepts( new PossiblyUndefinedStringArrayOffset( 'Possibly undefined array offset \'' . $offset_type->getId() . '\' ' . 'is risky given expected type \'' . $expected_offset_type->getId() . '\'.' . ' Consider using isset beforehand.', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } } /** * @return Type\Union */ public static function replaceOffsetTypeWithInts(Type\Union $offset_type) { $offset_types = $offset_type->getAtomicTypes(); $cloned = false; foreach ($offset_types as $key => $offset_type_part) { if ($offset_type_part instanceof Type\Atomic\TLiteralString) { if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) { if (!$cloned) { $offset_type = clone $offset_type; $cloned = true; } $offset_type->addType(new Type\Atomic\TLiteralInt((int) $offset_type_part->value)); $offset_type->removeType($key); } } elseif ($offset_type_part instanceof Type\Atomic\TBool) { if (!$cloned) { $offset_type = clone $offset_type; $cloned = true; } if ($offset_type_part instanceof Type\Atomic\TFalse) { if (!$offset_type->ignore_falsable_issues) { $offset_type->addType(new Type\Atomic\TLiteralInt(0)); $offset_type->removeType($key); } } elseif ($offset_type_part instanceof Type\Atomic\TTrue) { $offset_type->addType(new Type\Atomic\TLiteralInt(1)); $offset_type->removeType($key); } else { $offset_type->addType(new Type\Atomic\TLiteralInt(0)); $offset_type->addType(new Type\Atomic\TLiteralInt(1)); $offset_type->removeType($key); } } } return $offset_type; } private static function taintArrayType( StatementsAnalyzer $statements_analyzer, \Psalm\Codebase $codebase, PhpParser\Node\Expr $stmt, Type\Union $stmt_type, string $array_var_id ) : void { if ($codebase->taint && $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())) { if ($array_var_id === '$_GET' || $array_var_id === '$_POST' || $array_var_id === '$_COOKIE') { $taint_location = new CodeLocation($statements_analyzer->getSource(), $stmt); $server_taint_source = new Source( $array_var_id . ':' . $taint_location->file_name . ':' . $taint_location->raw_file_start, $array_var_id, $taint_location, null, Type\TaintKindGroup::ALL_INPUT ); $codebase->taint->addSource($server_taint_source); $stmt_type->parent_nodes = [ $server_taint_source ]; } } } }