Skip to content

Commit

Permalink
Add diagnostic message when shape fields are missing (#8762)
Browse files Browse the repository at this point in the history
* Add a simple diagnostic for missing array shape fields

* Fix dumb mistakes

* Allow nested shape extra keys to prompt warning too
  • Loading branch information
muglug committed Nov 26, 2022
1 parent d1921b1 commit 1819a2d
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 5 deletions.
Expand Up @@ -56,6 +56,7 @@
use function array_filter;
use function array_values;
use function count;
use function implode;
use function in_array;
use function strpos;
use function strtolower;
Expand Down Expand Up @@ -568,7 +569,13 @@ public static function verifyReturnType(
. $declared_return_type->getId()
. '\' for ' . $cased_method_id
. ' is incorrect, got \''
. $inferred_return_type->getId() . '\'',
. $inferred_return_type->getId()
. '\''
. ($union_comparison_results->missing_shape_fields
? ' which is different due to additional array shape fields ('
. implode(', ', $union_comparison_results->missing_shape_fields)
. ')'
: ''),
$return_type_location
),
$suppressed_issues,
Expand Down
Expand Up @@ -64,6 +64,7 @@

use function count;
use function explode;
use function implode;
use function in_array;
use function ord;
use function preg_split;
Expand Down Expand Up @@ -1044,8 +1045,14 @@ public static function verifyType(
} else {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', but ' . $type . ' provided',
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId()
. ', but ' . $type
. ($union_comparison_results->missing_shape_fields
? ' with additional array shape fields ('
. implode(', ', $union_comparison_results->missing_shape_fields)
. ') was'
: '')
. ' provided',
$arg_location,
$cased_method_id
),
Expand Down
8 changes: 7 additions & 1 deletion src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php
Expand Up @@ -45,6 +45,7 @@

use function count;
use function explode;
use function implode;
use function reset;
use function strtolower;

Expand Down Expand Up @@ -480,7 +481,12 @@ public static function analyze(
new InvalidReturnStatement(
'The inferred type \'' . $inferred_type->getId()
. '\' does not match the declared return '
. 'type \'' . $local_return_type->getId() . '\' for ' . $cased_method_id,
. 'type \'' . $local_return_type->getId() . '\' for ' . $cased_method_id
. ($union_comparison_results->missing_shape_fields
? ' due to additional array shape fields ('
. implode(', ', $union_comparison_results->missing_shape_fields)
. ')'
: ''),
new CodeLocation($source, $stmt->expr)
),
$statements_analyzer->getSuppressedIssues()
Expand Down
9 changes: 9 additions & 0 deletions src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php
Expand Up @@ -9,6 +9,7 @@
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TObjectWithProperties;

use function array_keys;
use function is_string;

/**
Expand Down Expand Up @@ -82,6 +83,11 @@ public static function isContainedBy(
) {
$atomic_comparison_result->type_coerced = true;
}

if ($property_type_comparison->missing_shape_fields) {
$atomic_comparison_result->missing_shape_fields
= $property_type_comparison->missing_shape_fields;
}
}

$all_types_contain = false;
Expand All @@ -95,6 +101,9 @@ public static function isContainedBy(
}
}
if ($container_sealed && $input_properties) {
if ($atomic_comparison_result) {
$atomic_comparison_result->missing_shape_fields = array_keys($input_properties);
}
return false;
}
return $all_types_contain;
Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Internal/Type/Comparator/TypeComparisonResult.php
Expand Up @@ -48,4 +48,7 @@ class TypeComparisonResult

/** @var ?Atomic */
public $replacement_atomic_type;

/** @var ?non-empty-list<int|string> */
public $missing_shape_fields;
}
10 changes: 10 additions & 0 deletions src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php
Expand Up @@ -100,6 +100,8 @@ public static function isContainedBy(
$some_type_coerced = false;
$some_type_coerced_from_mixed = false;

$some_missing_shape_fields = null;

if ($input_type_part instanceof TArrayKey
&& ($container_type->hasInt() && $container_type->hasString())
) {
Expand Down Expand Up @@ -272,6 +274,10 @@ public static function isContainedBy(
} else {
$all_type_coerced_from_as_mixed = true;
}

if ($atomic_comparison_result->missing_shape_fields) {
$some_missing_shape_fields = $atomic_comparison_result->missing_shape_fields;
}
}

if ($is_atomic_contained_by) {
Expand Down Expand Up @@ -331,6 +337,10 @@ public static function isContainedBy(
if (!$scalar_type_match_found) {
$union_comparison_result->scalar_type_match_found = false;
}

if ($some_missing_shape_fields && !$some_type_coerced && !$scalar_type_match_found) {
$union_comparison_result->missing_shape_fields = $some_missing_shape_fields;
}
}

return false;
Expand Down
4 changes: 3 additions & 1 deletion tests/ArgTest.php
Expand Up @@ -5,6 +5,8 @@
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;

use const DIRECTORY_SEPARATOR;

class ArgTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
Expand Down Expand Up @@ -792,7 +794,7 @@ function a(array $a): string {
$sealedExtraKeys = ["test" => "str", "somethingElse" => "test"];
a($sealedExtraKeys);
',
'error_message' => 'InvalidArgument',
'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:23 - Argument 1 of a expects array{test: string}, but array{somethingElse: \'test\', test: \'str\'} with additional array shape fields (somethingElse) was provided',
],
'callbackArgsCountMismatch' => [
'code' => '<?php
Expand Down

0 comments on commit 1819a2d

Please sign in to comment.