mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Merge remote-tracking branch 'upstream/4.x' into upstream-master
This commit is contained in:
commit
b633619a2c
25
bin/generate_issues_list_doc.php
Executable file
25
bin/generate_issues_list_doc.php
Executable file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
$docs_dir = dirname(__DIR__) . DIRECTORY_SEPARATOR . "docs"
|
||||
. DIRECTORY_SEPARATOR . "running_psalm" . DIRECTORY_SEPARATOR;
|
||||
$issues_index = "{$docs_dir}issues.md";
|
||||
$issues_dir = "{$docs_dir}issues";
|
||||
|
||||
if (!file_exists($issues_dir)) {
|
||||
throw new UnexpectedValueException("Issues documentation not found");
|
||||
}
|
||||
|
||||
$issues_list = array_filter(array_map(function (string $issue_file) {
|
||||
if ($issue_file === "." || $issue_file === ".." || substr($issue_file, -3) !== ".md") {
|
||||
return false;
|
||||
}
|
||||
|
||||
$issue = substr($issue_file, 0, strlen($issue_file) - 3);
|
||||
return " - [$issue](issues/$issue.md)";
|
||||
}, scandir($issues_dir)));
|
||||
|
||||
usort($issues_list, "strcasecmp");
|
||||
|
||||
$issues_md_contents = array_merge(["# Issue types", ""], $issues_list, [""]);
|
||||
file_put_contents($issues_index, implode("\n", $issues_md_contents));
|
@ -41,7 +41,13 @@
|
||||
<xs:attribute name="findUnusedCode" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="findUnusedVariablesAndParams" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="findUnusedPsalmSuppress" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="forbidEcho" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="forbidEcho" type="xs:boolean" default="false">
|
||||
<xs:annotation>
|
||||
<xs:documentation xml:lang="en">
|
||||
Deprecated. Will be replaced by adding echo to forbiddenFunctions in Psalm 5.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="hideExternalErrors" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="hoistConstants" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="ignoreInternalFunctionFalseReturn" type="xs:boolean" default="true" />
|
||||
@ -69,6 +75,7 @@
|
||||
<xs:attribute name="usePhpDocPropertiesWithoutMagicCall" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="skipChecksOnUnresolvableIncludes" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="sealAllMethods" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="sealAllProperties" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="runTaintAnalysis" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="usePhpStormMetaPath" type="xs:boolean" default="true" />
|
||||
<xs:attribute name="allowInternalNamedArgumentCalls" type="xs:boolean" default="true" />
|
||||
|
@ -1996,7 +1996,7 @@ return [
|
||||
'dio_tcsetattr' => ['bool', 'fd'=>'resource', 'options'=>'array'],
|
||||
'dio_truncate' => ['bool', 'fd'=>'resource', 'offset'=>'int'],
|
||||
'dio_write' => ['int', 'fd'=>'resource', 'data'=>'string', 'length='=>'int'],
|
||||
'dir' => ['Directory|false|null', 'directory'=>'string', 'context='=>'resource'],
|
||||
'dir' => ['Directory|false', 'directory'=>'string', 'context='=>'resource'],
|
||||
'Directory::close' => ['void', 'dir_handle='=>'resource'],
|
||||
'Directory::read' => ['string|false', 'dir_handle='=>'resource'],
|
||||
'Directory::rewind' => ['void', 'dir_handle='=>'resource'],
|
||||
|
@ -10375,7 +10375,7 @@ return [
|
||||
'dio_tcsetattr' => ['bool', 'fd'=>'resource', 'options'=>'array'],
|
||||
'dio_truncate' => ['bool', 'fd'=>'resource', 'offset'=>'int'],
|
||||
'dio_write' => ['int', 'fd'=>'resource', 'data'=>'string', 'length='=>'int'],
|
||||
'dir' => ['Directory|false|null', 'directory'=>'string', 'context='=>'resource'],
|
||||
'dir' => ['Directory|false', 'directory'=>'string', 'context='=>'resource'],
|
||||
'dirname' => ['string', 'path'=>'string', 'levels='=>'int'],
|
||||
'disk_free_space' => ['float|false', 'directory'=>'string'],
|
||||
'disk_total_space' => ['float|false', 'directory'=>'string'],
|
||||
|
@ -294,6 +294,16 @@ This defaults to `false`.
|
||||
|
||||
When `true`, Psalm will treat all classes as if they had sealed methods, meaning that if you implement the magic method `__call`, you also have to add `@method` for each magic method. Defaults to false.
|
||||
|
||||
#### sealAllProperties
|
||||
|
||||
```xml
|
||||
<psalm
|
||||
sealAllProperties="[bool]"
|
||||
>
|
||||
```
|
||||
|
||||
When `true`, Psalm will treat all classes as if they had sealed properties, meaning that Psalm will disallow getting and setting any properties not contained in a list of `@property` (or `@property-read`/`@property-write`) annotations and not explicitly defined as a `property`. Defaults to false.
|
||||
|
||||
#### runTaintAnalysis
|
||||
|
||||
```xml
|
||||
|
@ -7,6 +7,7 @@
|
||||
- [CircularReference](issues/CircularReference.md)
|
||||
- [ComplexFunction](issues/ComplexFunction.md)
|
||||
- [ComplexMethod](issues/ComplexMethod.md)
|
||||
- [ConfigIssue](issues/ConfigIssue.md)
|
||||
- [ConflictingReferenceConstraint](issues/ConflictingReferenceConstraint.md)
|
||||
- [ConstructorSignatureMismatch](issues/ConstructorSignatureMismatch.md)
|
||||
- [ContinueOutsideLoop](issues/ContinueOutsideLoop.md)
|
||||
@ -21,6 +22,8 @@
|
||||
- [DuplicateArrayKey](issues/DuplicateArrayKey.md)
|
||||
- [DuplicateClass](issues/DuplicateClass.md)
|
||||
- [DuplicateConstant](issues/DuplicateConstant.md)
|
||||
- [DuplicateEnumCase](issues/DuplicateEnumCase.md)
|
||||
- [DuplicateEnumCaseValue](issues/DuplicateEnumCaseValue.md)
|
||||
- [DuplicateFunction](issues/DuplicateFunction.md)
|
||||
- [DuplicateMethod](issues/DuplicateMethod.md)
|
||||
- [DuplicateParam](issues/DuplicateParam.md)
|
||||
@ -62,6 +65,7 @@
|
||||
- [InvalidDocblock](issues/InvalidDocblock.md)
|
||||
- [InvalidDocblockParamName](issues/InvalidDocblockParamName.md)
|
||||
- [InvalidEnumBackingType](issues/InvalidEnumBackingType.md)
|
||||
- [InvalidEnumCaseValue](issues/InvalidEnumCaseValue.md)
|
||||
- [InvalidExtendClass](issues/InvalidExtendClass.md)
|
||||
- [InvalidFalsableReturnType](issues/InvalidFalsableReturnType.md)
|
||||
- [InvalidFunctionCall](issues/InvalidFunctionCall.md)
|
||||
@ -132,6 +136,7 @@
|
||||
- [MoreSpecificReturnType](issues/MoreSpecificReturnType.md)
|
||||
- [MutableDependency](issues/MutableDependency.md)
|
||||
- [NamedArgumentNotAllowed](issues/NamedArgumentNotAllowed.md)
|
||||
- [NoEnumProperties](issues/NoEnumProperties.md)
|
||||
- [NoInterfaceProperties](issues/NoInterfaceProperties.md)
|
||||
- [NonInvariantDocblockPropertyType](issues/NonInvariantDocblockPropertyType.md)
|
||||
- [NonInvariantPropertyType](issues/NonInvariantPropertyType.md)
|
||||
@ -166,6 +171,7 @@
|
||||
- [PossiblyInvalidArrayOffset](issues/PossiblyInvalidArrayOffset.md)
|
||||
- [PossiblyInvalidCast](issues/PossiblyInvalidCast.md)
|
||||
- [PossiblyInvalidClone](issues/PossiblyInvalidClone.md)
|
||||
- [PossiblyInvalidDocblockTag](issues/PossiblyInvalidDocblockTag.md)
|
||||
- [PossiblyInvalidFunctionCall](issues/PossiblyInvalidFunctionCall.md)
|
||||
- [PossiblyInvalidIterator](issues/PossiblyInvalidIterator.md)
|
||||
- [PossiblyInvalidMethodCall](issues/PossiblyInvalidMethodCall.md)
|
||||
@ -193,6 +199,7 @@
|
||||
- [PossiblyUnusedMethod](issues/PossiblyUnusedMethod.md)
|
||||
- [PossiblyUnusedParam](issues/PossiblyUnusedParam.md)
|
||||
- [PossiblyUnusedProperty](issues/PossiblyUnusedProperty.md)
|
||||
- [PossiblyUnusedReturnValue](issues/PossiblyUnusedReturnValue.md)
|
||||
- [PropertyNotSetInConstructor](issues/PropertyNotSetInConstructor.md)
|
||||
- [PropertyTypeCoercion](issues/PropertyTypeCoercion.md)
|
||||
- [RawObjectIteration](issues/RawObjectIteration.md)
|
||||
@ -221,6 +228,7 @@
|
||||
- [TaintedSql](issues/TaintedSql.md)
|
||||
- [TaintedSSRF](issues/TaintedSSRF.md)
|
||||
- [TaintedSystemSecret](issues/TaintedSystemSecret.md)
|
||||
- [TaintedTextWithQuotes](issues/TaintedTextWithQuotes.md)
|
||||
- [TaintedUnserialize](issues/TaintedUnserialize.md)
|
||||
- [TaintedUserSecret](issues/TaintedUserSecret.md)
|
||||
- [TooFewArguments](issues/TooFewArguments.md)
|
||||
@ -259,14 +267,17 @@
|
||||
- [UnrecognizedExpression](issues/UnrecognizedExpression.md)
|
||||
- [UnrecognizedStatement](issues/UnrecognizedStatement.md)
|
||||
- [UnresolvableInclude](issues/UnresolvableInclude.md)
|
||||
- [UnsafeGenericInstantiation](issues/UnsafeGenericInstantiation.md)
|
||||
- [UnsafeInstantiation](issues/UnsafeInstantiation.md)
|
||||
- [UnusedClass](issues/UnusedClass.md)
|
||||
- [UnusedClosureParam](issues/UnusedClosureParam.md)
|
||||
- [UnusedConstructor](issues/UnusedConstructor.md)
|
||||
- [UnusedForeachValue](issues/UnusedForeachValue.md)
|
||||
- [UnusedFunctionCall](issues/UnusedFunctionCall.md)
|
||||
- [UnusedMethod](issues/UnusedMethod.md)
|
||||
- [UnusedMethodCall](issues/UnusedMethodCall.md)
|
||||
- [UnusedParam](issues/UnusedParam.md)
|
||||
- [UnusedProperty](issues/UnusedProperty.md)
|
||||
- [UnusedPsalmSuppress](issues/UnusedPsalmSuppress.md)
|
||||
- [UnusedReturnValue](issues/UnusedReturnValue.md)
|
||||
- [UnusedVariable](issues/UnusedVariable.md)
|
||||
|
3
docs/running_psalm/issues/ComplexFunction.md
Normal file
3
docs/running_psalm/issues/ComplexFunction.md
Normal file
@ -0,0 +1,3 @@
|
||||
# ComplexFunction
|
||||
|
||||
Emitted when a function is too complicated. Complicated functions should be split up.
|
3
docs/running_psalm/issues/ComplexMethod.md
Normal file
3
docs/running_psalm/issues/ComplexMethod.md
Normal file
@ -0,0 +1,3 @@
|
||||
# ComplexMethod
|
||||
|
||||
Emitted when a method is too complicated. Complicated methods should be split up.
|
@ -1,7 +1,7 @@
|
||||
# ForbiddenEcho
|
||||
|
||||
Emitted when Psalm encounters an echo statement and the `forbidEcho` flag in your config is set to `true`
|
||||
|
||||
This issue is deprecated and will be removed in Psalm 5. Adding echo to forbiddenFunctions in config will result in ForbiddenCode issue instead
|
||||
```php
|
||||
<?php
|
||||
|
||||
|
@ -344,6 +344,11 @@ class Config
|
||||
*/
|
||||
public $seal_all_methods = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $seal_all_properties = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
@ -910,6 +915,7 @@ class Config
|
||||
'reportMixedIssues' => 'show_mixed_issues',
|
||||
'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes',
|
||||
'sealAllMethods' => 'seal_all_methods',
|
||||
'sealAllProperties' => 'seal_all_properties',
|
||||
'runTaintAnalysis' => 'run_taint_analysis',
|
||||
'usePhpStormMetaPath' => 'use_phpstorm_meta_path',
|
||||
'allowInternalNamedArgumentsCalls' => 'allow_internal_named_arg_calls',
|
||||
@ -1180,21 +1186,23 @@ class Config
|
||||
}
|
||||
|
||||
if (isset($config_xml->issueHandlers)) {
|
||||
/** @var SimpleXMLElement $issue_handler */
|
||||
foreach ($config_xml->issueHandlers->children() as $key => $issue_handler) {
|
||||
if ($key === 'PluginIssue') {
|
||||
$custom_class_name = (string) $issue_handler['name'];
|
||||
/** @var string $key */
|
||||
$config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
|
||||
$issue_handler,
|
||||
$base_dir
|
||||
);
|
||||
} else {
|
||||
/** @var string $key */
|
||||
$config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
|
||||
$issue_handler,
|
||||
$base_dir
|
||||
);
|
||||
foreach ($config_xml->issueHandlers as $issue_handlers) {
|
||||
/** @var SimpleXMLElement $issue_handler */
|
||||
foreach ($issue_handlers->children() as $key => $issue_handler) {
|
||||
if ($key === 'PluginIssue') {
|
||||
$custom_class_name = (string) $issue_handler['name'];
|
||||
/** @var string $key */
|
||||
$config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
|
||||
$issue_handler,
|
||||
$base_dir
|
||||
);
|
||||
} else {
|
||||
/** @var string $key */
|
||||
$config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
|
||||
$issue_handler,
|
||||
$base_dir
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1938,6 +1938,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
$method_analyzer,
|
||||
$interface_return_type,
|
||||
$interface_class,
|
||||
$original_fq_classlike_name,
|
||||
$interface_return_type_location,
|
||||
[$analyzed_method_id->__toString()],
|
||||
$did_explicitly_return
|
||||
@ -1963,6 +1964,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
$method_analyzer,
|
||||
$return_type,
|
||||
$fq_classlike_name,
|
||||
$original_fq_classlike_name,
|
||||
$return_type_location,
|
||||
$overridden_method_ids,
|
||||
$did_explicitly_return
|
||||
|
@ -80,6 +80,7 @@ class ReturnTypeAnalyzer
|
||||
FunctionLikeAnalyzer $function_like_analyzer,
|
||||
?Union $return_type = null,
|
||||
?string $fq_class_name = null,
|
||||
?string $static_fq_class_name = null,
|
||||
?CodeLocation $return_type_location = null,
|
||||
array $compatible_method_ids = [],
|
||||
bool $did_explicitly_return = false,
|
||||
@ -421,7 +422,7 @@ class ReturnTypeAnalyzer
|
||||
$codebase,
|
||||
$return_type,
|
||||
$self_fq_class_name,
|
||||
$self_fq_class_name,
|
||||
$static_fq_class_name,
|
||||
$parent_class,
|
||||
true,
|
||||
true,
|
||||
|
@ -464,6 +464,12 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
$this->track_mutations = true;
|
||||
}
|
||||
|
||||
if ($this->function instanceof ArrowFunction && $storage->return_type && $storage->return_type->isNever()) {
|
||||
// ArrowFunction perform a return implicitly so if the return type is never, we have to suppress the error
|
||||
// note: the never can only come from phpdoc. PHP will refuse short closures with never in signature
|
||||
$statements_analyzer->addSuppressedIssues(['NoValue']);
|
||||
}
|
||||
|
||||
$statements_analyzer->analyze($function_stmts, $context, $global_context, true);
|
||||
|
||||
if ($codebase->alter_code
|
||||
@ -1413,6 +1419,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
$this,
|
||||
$return_type,
|
||||
$fq_class_name,
|
||||
$fq_class_name,
|
||||
$return_type_location,
|
||||
[],
|
||||
$did_explicitly_return,
|
||||
|
@ -91,6 +91,16 @@ class ScopeAnalyzer
|
||||
($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_)
|
||||
) {
|
||||
if (!$return_is_exit && $stmt instanceof PhpParser\Node\Stmt\Return_) {
|
||||
$stmt_return_type = null;
|
||||
if ($nodes && $stmt->expr) {
|
||||
$stmt_return_type = $nodes->getType($stmt->expr);
|
||||
}
|
||||
|
||||
// don't consider a return if the expression never returns (e.g. a throw inside a short closure)
|
||||
if ($stmt_return_type && ($stmt_return_type->isNever() || $stmt_return_type->isEmpty())) {
|
||||
return array_values(array_unique(array_merge($control_actions, [self::ACTION_END])));
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_merge($control_actions, [self::ACTION_RETURN])));
|
||||
}
|
||||
|
||||
|
@ -38,20 +38,27 @@ use Psalm\Storage\MethodStorage;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TCallable;
|
||||
use Psalm\Type\Atomic\TCallableArray;
|
||||
use Psalm\Type\Atomic\TCallableKeyedArray;
|
||||
use Psalm\Type\Atomic\TCallableList;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TList;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TNonEmptyArray;
|
||||
use Psalm\Type\Atomic\TNonEmptyList;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_map;
|
||||
use function array_reverse;
|
||||
use function array_slice;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function max;
|
||||
use function min;
|
||||
use function reset;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
@ -557,43 +564,12 @@ class ArgumentsAnalyzer
|
||||
|
||||
$has_packed_var = false;
|
||||
|
||||
$packed_var_definite_args = 0;
|
||||
|
||||
foreach ($args as $arg) {
|
||||
if ($arg->unpack) {
|
||||
$arg_value_type = $statements_analyzer->node_data->getType($arg->value);
|
||||
|
||||
if (!$arg_value_type
|
||||
|| !$arg_value_type->isSingle()
|
||||
|| !$arg_value_type->hasArray()
|
||||
) {
|
||||
$has_packed_var = true;
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($arg_value_type->getAtomicTypes() as $atomic_arg_type) {
|
||||
if (!$atomic_arg_type instanceof TKeyedArray) {
|
||||
$has_packed_var = true;
|
||||
break 2;
|
||||
}
|
||||
|
||||
$packed_var_definite_args = 0;
|
||||
|
||||
foreach ($atomic_arg_type->properties as $property_type) {
|
||||
if ($property_type->possibly_undefined) {
|
||||
$has_packed_var = true;
|
||||
} else {
|
||||
$packed_var_definite_args++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$has_packed_var = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_packed_var) {
|
||||
$packed_var_definite_args = max(0, $packed_var_definite_args - 1);
|
||||
}
|
||||
|
||||
$last_param = $function_params
|
||||
? $function_params[count($function_params) - 1]
|
||||
: null;
|
||||
@ -951,9 +927,7 @@ class ArgumentsAnalyzer
|
||||
$in_call_map,
|
||||
$method_id,
|
||||
$cased_method_id,
|
||||
$code_location,
|
||||
$has_packed_var,
|
||||
$packed_var_definite_args
|
||||
$code_location
|
||||
);
|
||||
|
||||
return null;
|
||||
@ -1465,9 +1439,7 @@ class ArgumentsAnalyzer
|
||||
bool $in_call_map,
|
||||
$method_id,
|
||||
?string $cased_method_id,
|
||||
CodeLocation $code_location,
|
||||
bool $has_packed_var,
|
||||
int $packed_var_definite_args
|
||||
CodeLocation $code_location
|
||||
): void {
|
||||
if (!$is_variadic
|
||||
&& count($args) > count($function_params)
|
||||
@ -1491,48 +1463,113 @@ class ArgumentsAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$has_packed_var && count($args) < count($function_params)) {
|
||||
if ($function_storage) {
|
||||
$expected_param_count = $function_storage->required_param_count;
|
||||
} else {
|
||||
for ($i = 0, $j = count($function_params); $i < $j; ++$i) {
|
||||
$param = $function_params[$i];
|
||||
if (count($args) < count($function_params)) {
|
||||
//we're gonna loop over given args and unset them from the function_params.
|
||||
// If some mandatory params are left at the end, we'll throw an error
|
||||
foreach ($args as $arg) {
|
||||
// when the argument is not named, we can remove the params in order
|
||||
if ($arg->name === null) {
|
||||
// if we're unpacking, we try to unset the exact number of params, if we can't we give up and return
|
||||
if ($arg->unpack) {
|
||||
$arg_value_type = $statements_analyzer->node_data->getType($arg->value);
|
||||
|
||||
if ($param->is_optional || $param->is_variadic) {
|
||||
break;
|
||||
if (!$arg_value_type || !$arg_value_type->hasArray()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($arg_value_type->isSingle()
|
||||
&& ($atomic_arg_type = $arg_value_type->getSingleAtomic())
|
||||
&& $atomic_arg_type instanceof TKeyedArray
|
||||
&& !$atomic_arg_type->is_list
|
||||
) {
|
||||
//if we have a single shape, we'll check param names
|
||||
foreach ($atomic_arg_type->properties as $property_name => $_property_type) {
|
||||
foreach ($function_params as $k => $param) {
|
||||
if ($param->name === $property_name) {
|
||||
unset($function_params[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($arg_value_type->getAtomicTypes() as $atomic_arg_type) {
|
||||
$packed_var_definite_args_tmp = [];
|
||||
if ($atomic_arg_type instanceof TCallableArray ||
|
||||
$atomic_arg_type instanceof TCallableList ||
|
||||
$atomic_arg_type instanceof TCallableKeyedArray
|
||||
) {
|
||||
$packed_var_definite_args_tmp[] = 2;
|
||||
} elseif ($atomic_arg_type instanceof TKeyedArray) {
|
||||
if (!$atomic_arg_type->sealed) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($atomic_arg_type->properties as $property_type) {
|
||||
if ($property_type->possibly_undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
//we did not return. The number of packed params is the number of properties
|
||||
$packed_var_definite_args_tmp[] = count($atomic_arg_type->properties);
|
||||
} elseif ($atomic_arg_type instanceof TNonEmptyArray ||
|
||||
$atomic_arg_type instanceof TNonEmptyList
|
||||
) {
|
||||
if ($atomic_arg_type->count === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$packed_var_definite_args_tmp[] = $atomic_arg_type->count;
|
||||
} elseif ($atomic_arg_type instanceof TArray
|
||||
&& $atomic_arg_type->type_params[1]->isEmpty()
|
||||
) {
|
||||
$packed_var_definite_args_tmp[] = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (min($packed_var_definite_args_tmp) === max($packed_var_definite_args_tmp)) {
|
||||
//we have a stable number of params
|
||||
$packed_var_definite_args = $packed_var_definite_args_tmp[0];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//if we're not unpacking, we remove the first param
|
||||
$packed_var_definite_args = 1;
|
||||
}
|
||||
|
||||
$function_params = array_slice($function_params, $packed_var_definite_args);
|
||||
continue;
|
||||
}
|
||||
|
||||
$expected_param_count = $i;
|
||||
foreach ($function_params as $k => $param) {
|
||||
if ($param->name === $arg->name->name) {
|
||||
unset($function_params[$k]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = count($args) + $packed_var_definite_args, $j = count($function_params); $i < $j; ++$i) {
|
||||
$param = $function_params[$i];
|
||||
|
||||
if (!$param->is_optional
|
||||
&& !$param->is_variadic
|
||||
&& ($in_call_map
|
||||
|| !$function_storage instanceof MethodStorage
|
||||
|| $function_storage->is_static
|
||||
|| ($method_id instanceof MethodIdentifier
|
||||
&& $method_id->method_name === '__construct'))
|
||||
) {
|
||||
//we're now left with an array of params that were not passed.
|
||||
// If they're mandatory, throw an error. Otherwise, we compute the default value
|
||||
foreach ($function_params as $i => $param) {
|
||||
if (!$param->is_optional && !$param->is_variadic) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooFewArguments(
|
||||
'Too few arguments for ' . $cased_method_id
|
||||
. ' - expecting ' . $expected_param_count
|
||||
. ' but saw ' . (count($args) + $packed_var_definite_args),
|
||||
. ' - expecting ' . $param->name . ' to be passed',
|
||||
$code_location,
|
||||
(string)$method_id
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
);
|
||||
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($param->is_optional
|
||||
&& $param->type
|
||||
if ($param->type
|
||||
&& $param->default_type
|
||||
&& !$param->is_variadic
|
||||
&& $template_result
|
||||
|
@ -325,12 +325,13 @@ class FunctionCallReturnTypeFetcher
|
||||
if (!$call_args) {
|
||||
switch ($call_map_key) {
|
||||
case 'hrtime':
|
||||
return new Union([
|
||||
new TKeyedArray([
|
||||
Type::getInt(),
|
||||
Type::getInt()
|
||||
])
|
||||
$keyed_array = new TKeyedArray([
|
||||
Type::getInt(),
|
||||
Type::getInt()
|
||||
]);
|
||||
$keyed_array->sealed = true;
|
||||
$keyed_array->is_list = true;
|
||||
return new Union([$keyed_array]);
|
||||
|
||||
case 'get_called_class':
|
||||
return new Union([
|
||||
@ -438,20 +439,19 @@ class FunctionCallReturnTypeFetcher
|
||||
return $int;
|
||||
}
|
||||
|
||||
$keyed_array = new TKeyedArray([
|
||||
Type::getInt(),
|
||||
Type::getInt()
|
||||
]);
|
||||
$keyed_array->sealed = true;
|
||||
$keyed_array->is_list = true;
|
||||
|
||||
if ((string) $first_arg_type === 'false') {
|
||||
return new Union([
|
||||
new TKeyedArray([
|
||||
Type::getInt(),
|
||||
Type::getInt()
|
||||
])
|
||||
]);
|
||||
return new Union([$keyed_array]);
|
||||
}
|
||||
|
||||
return new Union([
|
||||
new TKeyedArray([
|
||||
Type::getInt(),
|
||||
Type::getInt()
|
||||
]),
|
||||
$keyed_array,
|
||||
new TInt()
|
||||
]);
|
||||
}
|
||||
|
@ -539,7 +539,7 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
case '__set':
|
||||
// If `@psalm-seal-properties` is set, the property must be defined with
|
||||
// a `@property` annotation
|
||||
if ($class_storage->sealed_properties
|
||||
if (($class_storage->sealed_properties || $codebase->config->seal_all_properties)
|
||||
&& !isset($class_storage->pseudo_property_set_types['$' . $prop_name])
|
||||
&& IssueBuffer::accepts(
|
||||
new UndefinedThisPropertyAssignment(
|
||||
@ -638,7 +638,7 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
case '__get':
|
||||
// If `@psalm-seal-properties` is set, the property must be defined with
|
||||
// a `@property` annotation
|
||||
if ($class_storage->sealed_properties
|
||||
if (($class_storage->sealed_properties || $codebase->config->seal_all_properties)
|
||||
&& !isset($class_storage->pseudo_property_get_types['$' . $prop_name])
|
||||
&& IssueBuffer::accepts(
|
||||
new UndefinedThisPropertyFetch(
|
||||
|
@ -676,7 +676,9 @@ class AtomicPropertyFetchAnalyzer
|
||||
* If we have an explicit list of all allowed magic properties on the class, and we're
|
||||
* not in that list, fall through
|
||||
*/
|
||||
if (!$class_storage->sealed_properties && !$override_property_visibility) {
|
||||
if (!($class_storage->sealed_properties || $codebase->config->seal_all_properties)
|
||||
&& !$override_property_visibility
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -103,6 +103,11 @@ class LanguageServer extends Dispatcher
|
||||
*/
|
||||
protected $onchange_paths_to_analyze = [];
|
||||
|
||||
/**
|
||||
* @var array<string, list<IssueData>>
|
||||
*/
|
||||
protected $current_issues = [];
|
||||
|
||||
public function __construct(
|
||||
ProtocolReader $reader,
|
||||
ProtocolWriter $writer,
|
||||
@ -365,6 +370,7 @@ class LanguageServer extends Dispatcher
|
||||
public function emitIssues(array $uris): void
|
||||
{
|
||||
$data = IssueBuffer::clear();
|
||||
$this->current_issues = $data;
|
||||
|
||||
foreach ($uris as $file_path => $uri) {
|
||||
$diagnostics = array_map(
|
||||
@ -560,4 +566,14 @@ class LanguageServer extends Dispatcher
|
||||
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of current_issues
|
||||
*
|
||||
* @return array<string, list<IssueData>>
|
||||
*/
|
||||
public function getCurrentIssues(): array
|
||||
{
|
||||
return $this->current_issues;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ use Psalm\Codebase;
|
||||
use Psalm\Exception\UnanalyzedFileException;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer;
|
||||
use Psalm\IssueBuffer;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_combine;
|
||||
@ -367,9 +366,15 @@ class TextDocument
|
||||
$this->codebase->analyzer->addFilesToAnalyze(
|
||||
array_combine($all_file_paths_to_analyze, $all_file_paths_to_analyze)
|
||||
);
|
||||
$this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false);
|
||||
|
||||
$issues = IssueBuffer::clear();
|
||||
try {
|
||||
$this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
error_log('codeAction errored on file ' . $file_path. ', Reason: '.$e->getMessage());
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$issues = $this->server->getCurrentIssues();
|
||||
|
||||
if (empty($issues[$file_path])) {
|
||||
return new Success(null);
|
||||
|
@ -53,6 +53,7 @@ class ArrayMergeReturnTypeProvider implements FunctionReturnTypeProviderInterfac
|
||||
$codebase = $statements_source->getCodebase();
|
||||
|
||||
$generic_properties = [];
|
||||
$class_strings = [];
|
||||
$all_keyed_arrays = true;
|
||||
$all_int_offsets = true;
|
||||
$all_nonempty_lists = true;
|
||||
@ -110,6 +111,10 @@ class ArrayMergeReturnTypeProvider implements FunctionReturnTypeProviderInterfac
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($unpacked_type_part->class_strings[$key])) {
|
||||
$class_strings[$key] = true;
|
||||
}
|
||||
|
||||
if (!isset($generic_properties[$key]) || !$type->possibly_undefined) {
|
||||
$generic_properties[$key] = $type;
|
||||
} else {
|
||||
@ -221,6 +226,10 @@ class ArrayMergeReturnTypeProvider implements FunctionReturnTypeProviderInterfac
|
||||
) {
|
||||
$objectlike = new TKeyedArray($generic_properties);
|
||||
|
||||
if ($class_strings !== []) {
|
||||
$objectlike->class_strings = $class_strings;
|
||||
}
|
||||
|
||||
if ($all_nonempty_lists || $all_int_offsets) {
|
||||
$objectlike->is_list = true;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TArrayKey;
|
||||
use Psalm\Type\Atomic\TClassConstant;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TEnumCase;
|
||||
@ -36,7 +37,9 @@ use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNumeric;
|
||||
use Psalm\Type\Atomic\TPositiveInt;
|
||||
use Psalm\Type\Atomic\TScalar;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Reconciler;
|
||||
@ -929,7 +932,27 @@ class AssertionReconciler extends Reconciler
|
||||
|
||||
$scalar_type = substr($assertion, 0, $bracket_pos);
|
||||
|
||||
$existing_var_atomic_types = $existing_var_type->getAtomicTypes();
|
||||
$existing_var_atomic_types = [];
|
||||
|
||||
foreach ($existing_var_type->getAtomicTypes() as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TClassConstant) {
|
||||
$expanded = TypeExpander::expandAtomic(
|
||||
$statements_analyzer->getCodebase(),
|
||||
$existing_var_atomic_type,
|
||||
$existing_var_atomic_type->fq_classlike_name,
|
||||
$existing_var_atomic_type->fq_classlike_name,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
foreach ($expanded as $atomic_type) {
|
||||
$existing_var_atomic_types[$atomic_type->getKey()] = $atomic_type;
|
||||
}
|
||||
} else {
|
||||
$existing_var_atomic_types[$existing_var_atomic_type->getKey()] = $existing_var_atomic_type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scalar_type === 'int') {
|
||||
return self::handleLiteralEqualityWithInt(
|
||||
@ -938,6 +961,7 @@ class AssertionReconciler extends Reconciler
|
||||
$bracket_pos,
|
||||
$is_loose_equality,
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_types,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$negated,
|
||||
@ -950,209 +974,34 @@ class AssertionReconciler extends Reconciler
|
||||
|| $scalar_type === 'callable-string'
|
||||
|| $scalar_type === 'trait-string'
|
||||
) {
|
||||
if ($existing_var_type->hasMixed()
|
||||
|| $existing_var_type->hasScalar()
|
||||
|| $existing_var_type->hasArrayKey()
|
||||
) {
|
||||
if ($is_loose_equality) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
if ($scalar_type === 'class-string'
|
||||
|| $scalar_type === 'interface-string'
|
||||
|| $scalar_type === 'trait-string'
|
||||
) {
|
||||
return new Union([new TLiteralClassString($value)]);
|
||||
}
|
||||
|
||||
return new Union([new TLiteralString($value)]);
|
||||
}
|
||||
|
||||
$has_string = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TString) {
|
||||
$has_string = true;
|
||||
} elseif ($existing_var_atomic_type instanceof TTemplateParam) {
|
||||
if ($existing_var_atomic_type->as->hasMixed()
|
||||
|| $existing_var_atomic_type->as->hasString()
|
||||
|| $existing_var_atomic_type->as->hasScalar()
|
||||
|| $existing_var_atomic_type->as->hasArrayKey()
|
||||
) {
|
||||
if ($is_loose_equality) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
$existing_var_atomic_type = clone $existing_var_atomic_type;
|
||||
|
||||
$existing_var_atomic_type->as = self::handleLiteralEquality(
|
||||
$statements_analyzer,
|
||||
$assertion,
|
||||
$bracket_pos,
|
||||
false,
|
||||
$existing_var_atomic_type->as,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type->as->hasString()) {
|
||||
$has_string = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($has_string) {
|
||||
$existing_string_types = $existing_var_type->getLiteralStrings();
|
||||
|
||||
if ($existing_string_types) {
|
||||
$can_be_equal = false;
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $atomic_key => $_) {
|
||||
if ($atomic_key !== $assertion) {
|
||||
$existing_var_type->removeType($atomic_key);
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$can_be_equal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($var_id
|
||||
&& $code_location
|
||||
&& (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
|
||||
) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
$can_be_equal,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ($scalar_type === 'class-string'
|
||||
|| $scalar_type === 'interface-string'
|
||||
|| $scalar_type === 'trait-string'
|
||||
) {
|
||||
$existing_var_type = new Union([new TLiteralClassString($value)]);
|
||||
} else {
|
||||
$existing_var_type = new Union([new TLiteralString($value)]);
|
||||
}
|
||||
}
|
||||
} elseif ($var_id && $code_location && !$is_loose_equality) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
false,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
return self::handleLiteralEqualityWithString(
|
||||
$statements_analyzer,
|
||||
$assertion,
|
||||
$scalar_type,
|
||||
$bracket_pos,
|
||||
$is_loose_equality,
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_types,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
} elseif ($scalar_type === 'float') {
|
||||
$value = (float) $value;
|
||||
|
||||
if ($existing_var_type->hasMixed() || $existing_var_type->hasScalar() || $existing_var_type->hasNumeric()) {
|
||||
if ($is_loose_equality) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
return new Union([new TLiteralFloat($value)]);
|
||||
}
|
||||
|
||||
if ($existing_var_type->hasFloat()) {
|
||||
$existing_float_types = $existing_var_type->getLiteralFloats();
|
||||
|
||||
if ($existing_float_types) {
|
||||
$can_be_equal = false;
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $atomic_key => $_) {
|
||||
if ($atomic_key !== $assertion) {
|
||||
$existing_var_type->removeType($atomic_key);
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$can_be_equal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($var_id
|
||||
&& $code_location
|
||||
&& (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
|
||||
) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
$can_be_equal,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$existing_var_type = new Union([new TLiteralFloat($value)]);
|
||||
}
|
||||
} elseif ($var_id && $code_location && !$is_loose_equality) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
false,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
} elseif ($is_loose_equality && $existing_var_type->hasInt()) {
|
||||
// convert ints to floats
|
||||
$existing_float_types = $existing_var_type->getLiteralInts();
|
||||
|
||||
if ($existing_float_types) {
|
||||
$can_be_equal = false;
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $atomic_key => $_) {
|
||||
if (strpos($atomic_key, 'int(') === 0) {
|
||||
$atomic_key = 'float(' . substr($atomic_key, 4);
|
||||
}
|
||||
if ($atomic_key !== $assertion) {
|
||||
$existing_var_type->removeType($atomic_key);
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$can_be_equal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($var_id
|
||||
&& $code_location
|
||||
&& (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
|
||||
) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
$can_be_equal,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return self::handleLiteralEqualityWithFloat(
|
||||
$statements_analyzer,
|
||||
$assertion,
|
||||
$bracket_pos,
|
||||
$is_loose_equality,
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_types,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
} elseif ($scalar_type === 'enum') {
|
||||
[$fq_enum_name, $case_name] = explode('::', $value);
|
||||
|
||||
@ -1204,7 +1053,8 @@ class AssertionReconciler extends Reconciler
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $suppressed_issues
|
||||
* @param array<string, Atomic> $existing_var_atomic_types
|
||||
* @param string[] $suppressed_issues
|
||||
*/
|
||||
private static function handleLiteralEqualityWithInt(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
@ -1212,6 +1062,7 @@ class AssertionReconciler extends Reconciler
|
||||
int $bracket_pos,
|
||||
bool $is_loose_equality,
|
||||
Union $existing_var_type,
|
||||
array $existing_var_atomic_types,
|
||||
string $old_var_type_string,
|
||||
?string $var_id,
|
||||
bool $negated,
|
||||
@ -1220,92 +1071,119 @@ class AssertionReconciler extends Reconciler
|
||||
): Union {
|
||||
$value = (int) substr($assertion, $bracket_pos + 1, -1);
|
||||
|
||||
$compatible_int_type = self::getCompatibleIntType($existing_var_type, $value, $is_loose_equality);
|
||||
// we create the literal that is being asserted. We'll return this when we're sure this is the resulting type
|
||||
$literal_asserted_type = new Union([new TLiteralInt($value)]);
|
||||
$literal_asserted_type->from_docblock = $existing_var_type->from_docblock;
|
||||
|
||||
$compatible_int_type = self::getCompatibleIntType(
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_types,
|
||||
$value,
|
||||
$is_loose_equality
|
||||
);
|
||||
|
||||
if ($compatible_int_type !== null) {
|
||||
return $compatible_int_type;
|
||||
}
|
||||
|
||||
$has_int = false;
|
||||
$existing_var_atomic_types = $existing_var_type->getAtomicTypes();
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TInt) {
|
||||
$has_int = true;
|
||||
} elseif ($existing_var_atomic_type instanceof TTemplateParam) {
|
||||
$compatible_int_type = self::getCompatibleIntType($existing_var_type, $value, $is_loose_equality);
|
||||
if ($existing_var_atomic_type instanceof TPositiveInt && $value > 0) {
|
||||
return $literal_asserted_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TIntRange && $existing_var_atomic_type->contains($value)) {
|
||||
return $literal_asserted_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TLiteralInt && $existing_var_atomic_type->value === $value) {
|
||||
//if we're here, we check that we had at least another type in the union, otherwise it's redundant
|
||||
|
||||
if ($existing_var_type->isSingleIntLiteral()) {
|
||||
if ($var_id && $code_location) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
true,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
return $existing_var_type;
|
||||
}
|
||||
return $literal_asserted_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TInt && !$existing_var_atomic_type instanceof TLiteralInt) {
|
||||
return $literal_asserted_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TTemplateParam) {
|
||||
$compatible_int_type = self::getCompatibleIntType(
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_type->as->getAtomicTypes(),
|
||||
$value,
|
||||
$is_loose_equality
|
||||
);
|
||||
if ($compatible_int_type !== null) {
|
||||
return $compatible_int_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type->as->hasInt()) {
|
||||
$has_int = true;
|
||||
}
|
||||
} elseif ($existing_var_atomic_type instanceof TClassConstant) {
|
||||
$expanded = TypeExpander::expandAtomic(
|
||||
$statements_analyzer->getCodebase(),
|
||||
$existing_var_atomic_type,
|
||||
$existing_var_atomic_type->fq_classlike_name,
|
||||
$existing_var_atomic_type->fq_classlike_name,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
$existing_var_atomic_type = clone $existing_var_atomic_type;
|
||||
|
||||
$existing_var_atomic_type->as = self::handleLiteralEquality(
|
||||
$statements_analyzer,
|
||||
$assertion,
|
||||
$bracket_pos,
|
||||
false,
|
||||
$existing_var_atomic_type->as,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
|
||||
foreach ($expanded as $expanded_type) {
|
||||
$compatible_int_type = self::getCompatibleIntType(
|
||||
$existing_var_type,
|
||||
$value,
|
||||
$is_loose_equality
|
||||
);
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
|
||||
if ($compatible_int_type !== null) {
|
||||
return $compatible_int_type;
|
||||
}
|
||||
if ($is_loose_equality
|
||||
&& $existing_var_atomic_type instanceof TLiteralFloat
|
||||
&& (int)$existing_var_atomic_type->value === $value
|
||||
) {
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
|
||||
if ($expanded_type instanceof TInt) {
|
||||
$has_int = true;
|
||||
}
|
||||
if ($is_loose_equality
|
||||
&& $existing_var_atomic_type instanceof TLiteralString
|
||||
&& (int)$existing_var_atomic_type->value === $value
|
||||
) {
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
}
|
||||
|
||||
//here we'll accept non-literal type that *could* match on loose equality and return the original type
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
//here we'll accept non-literal type that *could* match on loose equality and return the original type
|
||||
if ($is_loose_equality) {
|
||||
if ($existing_var_atomic_type instanceof TString
|
||||
&& !$existing_var_atomic_type instanceof TLiteralString
|
||||
) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TFloat
|
||||
&& !$existing_var_atomic_type instanceof TLiteralFloat
|
||||
) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($has_int) {
|
||||
$existing_int_types = $existing_var_type->getLiteralInts();
|
||||
|
||||
if ($existing_int_types) {
|
||||
$can_be_equal = false;
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $atomic_key => $atomic_type) {
|
||||
if ($atomic_key !== $assertion
|
||||
&& !($atomic_type instanceof TPositiveInt && $value > 0)
|
||||
&& !($atomic_type instanceof TIntRange && $atomic_type->contains($value))
|
||||
) {
|
||||
$existing_var_type->removeType($atomic_key);
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$can_be_equal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($var_id
|
||||
&& $code_location
|
||||
&& (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
|
||||
) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
$can_be_equal,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$existing_var_type = new Union([new TLiteralInt($value)]);
|
||||
}
|
||||
} elseif ($var_id && $code_location && !$is_loose_equality) {
|
||||
//if we're here, no type was eligible for the given literal. We'll emit an impossible error for this assertion
|
||||
if ($var_id && $code_location) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
@ -1316,62 +1194,411 @@ class AssertionReconciler extends Reconciler
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
} elseif ($is_loose_equality && $existing_var_type->hasFloat()) {
|
||||
// convert floats to ints
|
||||
$existing_float_types = $existing_var_type->getLiteralFloats();
|
||||
}
|
||||
|
||||
if ($existing_float_types) {
|
||||
$can_be_equal = false;
|
||||
$did_remove_type = false;
|
||||
return Type::getNever();
|
||||
}
|
||||
|
||||
foreach ($existing_var_atomic_types as $atomic_key => $_) {
|
||||
if (strpos($atomic_key, 'float(') === 0) {
|
||||
$atomic_key = 'int(' . substr($atomic_key, 6);
|
||||
}
|
||||
if ($atomic_key !== $assertion) {
|
||||
$existing_var_type->removeType($atomic_key);
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$can_be_equal = true;
|
||||
/**
|
||||
* @param array<string, Atomic> $existing_var_atomic_types
|
||||
* @param string[] $suppressed_issues
|
||||
*/
|
||||
private static function handleLiteralEqualityWithString(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
string $assertion,
|
||||
string $scalar_type,
|
||||
int $bracket_pos,
|
||||
bool $is_loose_equality,
|
||||
Union $existing_var_type,
|
||||
array $existing_var_atomic_types,
|
||||
string $old_var_type_string,
|
||||
?string $var_id,
|
||||
bool $negated,
|
||||
?CodeLocation $code_location,
|
||||
array $suppressed_issues
|
||||
): Union {
|
||||
$value = substr($assertion, $bracket_pos + 1, -1);
|
||||
|
||||
// we create the literal that is being asserted. We'll return this when we're sure this is the resulting type
|
||||
$literal_asserted_type_string = new Union([new TLiteralString($value)]);
|
||||
$literal_asserted_type_string->from_docblock = $existing_var_type->from_docblock;
|
||||
$literal_asserted_type_classstring = new Union([new TLiteralClassString($value)]);
|
||||
$literal_asserted_type_classstring->from_docblock = $existing_var_type->from_docblock;
|
||||
|
||||
$compatible_string_type = self::getCompatibleStringType(
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_types,
|
||||
$value,
|
||||
$scalar_type,
|
||||
$is_loose_equality
|
||||
);
|
||||
|
||||
if ($compatible_string_type !== null) {
|
||||
return $compatible_string_type;
|
||||
}
|
||||
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TLiteralString && $existing_var_atomic_type->value === $value) {
|
||||
//if we're here, we check that we had at least another type in the union, otherwise it's redundant
|
||||
|
||||
if ($existing_var_type->isSingleStringLiteral()) {
|
||||
if ($var_id && $code_location) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
true,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
if ($var_id
|
||||
&& $code_location
|
||||
&& (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
|
||||
if ($scalar_type === 'class-string'
|
||||
|| $scalar_type === 'interface-string'
|
||||
|| $scalar_type === 'trait-string'
|
||||
) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
$can_be_equal,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
return $literal_asserted_type_classstring;
|
||||
}
|
||||
|
||||
return $literal_asserted_type_string;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TString && !$existing_var_atomic_type instanceof TLiteralString) {
|
||||
if ($scalar_type === 'class-string'
|
||||
|| $scalar_type === 'interface-string'
|
||||
|| $scalar_type === 'trait-string'
|
||||
) {
|
||||
return $literal_asserted_type_classstring;
|
||||
}
|
||||
|
||||
return $literal_asserted_type_string;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TTemplateParam) {
|
||||
$compatible_string_type = self::getCompatibleStringType(
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_type->as->getAtomicTypes(),
|
||||
$value,
|
||||
$scalar_type,
|
||||
$is_loose_equality
|
||||
);
|
||||
if ($compatible_string_type !== null) {
|
||||
return $compatible_string_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type->as->hasString()) {
|
||||
if ($scalar_type === 'class-string'
|
||||
|| $scalar_type === 'interface-string'
|
||||
|| $scalar_type === 'trait-string'
|
||||
) {
|
||||
return $literal_asserted_type_classstring;
|
||||
}
|
||||
|
||||
return $literal_asserted_type_string;
|
||||
}
|
||||
|
||||
$existing_var_atomic_type = clone $existing_var_atomic_type;
|
||||
|
||||
$existing_var_atomic_type->as = self::handleLiteralEquality(
|
||||
$statements_analyzer,
|
||||
$assertion,
|
||||
$bracket_pos,
|
||||
false,
|
||||
$existing_var_atomic_type->as,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
|
||||
if ($is_loose_equality
|
||||
&& $existing_var_atomic_type instanceof TLiteralInt
|
||||
&& (string)$existing_var_atomic_type->value === $value
|
||||
) {
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
|
||||
if ($is_loose_equality
|
||||
&& $existing_var_atomic_type instanceof TLiteralFloat
|
||||
&& (string)$existing_var_atomic_type->value === $value
|
||||
) {
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
}
|
||||
|
||||
//here we'll accept non-literal type that *could* match on loose equality and return the original type
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
//here we'll accept non-literal type that *could* match on loose equality and return the original type
|
||||
if ($is_loose_equality) {
|
||||
if ($existing_var_atomic_type instanceof TInt
|
||||
&& !$existing_var_atomic_type instanceof TLiteralInt
|
||||
) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TFloat
|
||||
&& !$existing_var_atomic_type instanceof TLiteralFloat
|
||||
) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $existing_var_type;
|
||||
//if we're here, no type was eligible for the given literal. We'll emit an impossible error for this assertion
|
||||
if ($var_id && $code_location) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
false,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
|
||||
return Type::getNever();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Atomic> $existing_var_atomic_types
|
||||
* @param string[] $suppressed_issues
|
||||
*/
|
||||
private static function handleLiteralEqualityWithFloat(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
string $assertion,
|
||||
int $bracket_pos,
|
||||
bool $is_loose_equality,
|
||||
Union $existing_var_type,
|
||||
array $existing_var_atomic_types,
|
||||
string $old_var_type_string,
|
||||
?string $var_id,
|
||||
bool $negated,
|
||||
?CodeLocation $code_location,
|
||||
array $suppressed_issues
|
||||
): Union {
|
||||
$value = (float)substr($assertion, $bracket_pos + 1, -1);
|
||||
|
||||
// we create the literal that is being asserted. We'll return this when we're sure this is the resulting type
|
||||
$literal_asserted_type = new Union([new TLiteralFloat($value)]);
|
||||
$literal_asserted_type->from_docblock = $existing_var_type->from_docblock;
|
||||
|
||||
$compatible_float_type = self::getCompatibleFloatType(
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_types,
|
||||
$value,
|
||||
$is_loose_equality
|
||||
);
|
||||
|
||||
if ($compatible_float_type !== null) {
|
||||
return $compatible_float_type;
|
||||
}
|
||||
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TLiteralFloat && $existing_var_atomic_type->value === $value) {
|
||||
//if we're here, we check that we had at least another type in the union, otherwise it's redundant
|
||||
|
||||
if ($existing_var_type->isSingleFloatLiteral()) {
|
||||
if ($var_id && $code_location) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
true,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
return $literal_asserted_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TFloat && !$existing_var_atomic_type instanceof TLiteralFloat) {
|
||||
return $literal_asserted_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TTemplateParam) {
|
||||
$compatible_float_type = self::getCompatibleFloatType(
|
||||
$existing_var_type,
|
||||
$existing_var_atomic_type->as->getAtomicTypes(),
|
||||
$value,
|
||||
$is_loose_equality
|
||||
);
|
||||
if ($compatible_float_type !== null) {
|
||||
return $compatible_float_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type->as->hasFloat()) {
|
||||
return $literal_asserted_type;
|
||||
}
|
||||
|
||||
$existing_var_atomic_type = clone $existing_var_atomic_type;
|
||||
|
||||
$existing_var_atomic_type->as = self::handleLiteralEquality(
|
||||
$statements_analyzer,
|
||||
$assertion,
|
||||
$bracket_pos,
|
||||
false,
|
||||
$existing_var_atomic_type->as,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
|
||||
if ($is_loose_equality
|
||||
&& $existing_var_atomic_type instanceof TLiteralInt
|
||||
&& (float)$existing_var_atomic_type->value === $value
|
||||
) {
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
|
||||
if ($is_loose_equality
|
||||
&& $existing_var_atomic_type instanceof TLiteralString
|
||||
&& (float)$existing_var_atomic_type->value === $value
|
||||
) {
|
||||
return new Union([$existing_var_atomic_type]);
|
||||
}
|
||||
}
|
||||
|
||||
//here we'll accept non-literal type that *could* match on loose equality and return the original type
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($is_loose_equality) {
|
||||
if ($existing_var_atomic_type instanceof TInt
|
||||
&& !$existing_var_atomic_type instanceof TLiteralInt
|
||||
) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
if ($existing_var_atomic_type instanceof TString
|
||||
&& !$existing_var_atomic_type instanceof TLiteralString
|
||||
) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if we're here, no type was eligible for the given literal. We'll emit an impossible error for this assertion
|
||||
if ($var_id && $code_location) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$var_id,
|
||||
$assertion,
|
||||
false,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
|
||||
return Type::getNever();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Atomic> $existing_var_atomic_types
|
||||
*/
|
||||
private static function getCompatibleIntType(
|
||||
Union $existing_var_type,
|
||||
array $existing_var_atomic_types,
|
||||
int $value,
|
||||
bool $is_loose_equality
|
||||
): ?Union {
|
||||
if ($existing_var_type->hasMixed()
|
||||
|| $existing_var_type->hasScalar()
|
||||
|| $existing_var_type->hasNumeric()
|
||||
|| $existing_var_type->hasArrayKey()
|
||||
) {
|
||||
if ($is_loose_equality) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TMixed
|
||||
|| $existing_var_atomic_type instanceof TScalar
|
||||
|| $existing_var_atomic_type instanceof TNumeric
|
||||
|| $existing_var_atomic_type instanceof TArrayKey
|
||||
) {
|
||||
if ($is_loose_equality) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
return new Union([new TLiteralInt($value)]);
|
||||
$asserted_type = new Union([new TLiteralInt($value)]);
|
||||
$asserted_type->from_docblock = $existing_var_type->from_docblock;
|
||||
return $asserted_type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Atomic> $existing_var_atomic_types
|
||||
*/
|
||||
private static function getCompatibleStringType(
|
||||
Union $existing_var_type,
|
||||
array $existing_var_atomic_types,
|
||||
string $value,
|
||||
string $scalar_type,
|
||||
bool $is_loose_equality
|
||||
): ?Union {
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TMixed
|
||||
|| $existing_var_atomic_type instanceof TScalar
|
||||
|| $existing_var_atomic_type instanceof TArrayKey
|
||||
) {
|
||||
if ($is_loose_equality) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
if ($scalar_type === 'class-string'
|
||||
|| $scalar_type === 'interface-string'
|
||||
|| $scalar_type === 'trait-string'
|
||||
) {
|
||||
$asserted_type = new Union([new TLiteralClassString($value)]);
|
||||
$asserted_type->from_docblock = $existing_var_type->from_docblock;
|
||||
return $asserted_type;
|
||||
}
|
||||
|
||||
$asserted_type = new Union([new TLiteralString($value)]);
|
||||
$asserted_type->from_docblock = $existing_var_type->from_docblock;
|
||||
return $asserted_type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Atomic> $existing_var_atomic_types
|
||||
*/
|
||||
private static function getCompatibleFloatType(
|
||||
Union $existing_var_type,
|
||||
array $existing_var_atomic_types,
|
||||
float $value,
|
||||
bool $is_loose_equality
|
||||
): ?Union {
|
||||
foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
|
||||
if ($existing_var_atomic_type instanceof TMixed
|
||||
|| $existing_var_atomic_type instanceof TScalar
|
||||
|| $existing_var_atomic_type instanceof TNumeric
|
||||
) {
|
||||
if ($is_loose_equality) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
$asserted_type = new Union([new TLiteralFloat($value)]);
|
||||
$asserted_type->from_docblock = $existing_var_type->from_docblock;
|
||||
return $asserted_type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -690,6 +690,24 @@ class ArgTest extends TestCase
|
||||
',
|
||||
'error_message' => 'ArgumentTypeCoercion',
|
||||
],
|
||||
'MissingMandatoryParamWithNamedParams' => [
|
||||
'<?php
|
||||
class User
|
||||
{
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected string $problematicOne,
|
||||
protected string $id = "",
|
||||
){}
|
||||
}
|
||||
|
||||
new User(
|
||||
name: "John",
|
||||
id: "asd",
|
||||
);
|
||||
',
|
||||
'error_message' => 'TooFewArguments',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -2121,6 +2121,24 @@ class ArrayFunctionCallTest extends TestCase
|
||||
return $a;
|
||||
}',
|
||||
],
|
||||
'keepClassStringInOffsetThroughArrayMerge' => [
|
||||
'<?php
|
||||
|
||||
class A {
|
||||
/** @var array<class-string, string> */
|
||||
private array $a;
|
||||
|
||||
public function __construct() {
|
||||
$this->a = [];
|
||||
}
|
||||
|
||||
public function handle(): void {
|
||||
$b = [A::class => "d"];
|
||||
$this->a = array_merge($this->a, $b);
|
||||
}
|
||||
}
|
||||
',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -735,6 +735,31 @@ class ClosureTest extends TestCase
|
||||
'ignored_issues' => [],
|
||||
'php_version' => '8.1',
|
||||
],
|
||||
'arrowFunctionReturnsNeverImplictly' => [
|
||||
'<?php
|
||||
$bar = ["foo", "bar"];
|
||||
|
||||
$bam = array_map(
|
||||
fn(string $a) => throw new Exception($a),
|
||||
$bar
|
||||
);',
|
||||
'assertions' => [],
|
||||
'error_levels' => [],
|
||||
'8.1'
|
||||
],
|
||||
'arrowFunctionReturnsNeverExplictly' => [
|
||||
'<?php
|
||||
$bar = ["foo", "bar"];
|
||||
|
||||
$bam = array_map(
|
||||
/** @return never */
|
||||
fn(string $a) => die(),
|
||||
$bar
|
||||
);',
|
||||
'assertions' => [],
|
||||
'error_levels' => [],
|
||||
'8.1'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,7 @@ class ConfigTest extends TestCase
|
||||
$config = $this->project_analyzer->getConfig();
|
||||
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
|
||||
}
|
||||
|
||||
public function testIgnoreProjectDirectory(): void
|
||||
@ -129,7 +129,7 @@ class ConfigTest extends TestCase
|
||||
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
|
||||
}
|
||||
|
||||
public function testIgnoreMissingProjectDirectory(): void
|
||||
@ -152,8 +152,8 @@ class ConfigTest extends TestCase
|
||||
$config = $this->project_analyzer->getConfig();
|
||||
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('does/not/exist/FileAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath(__DIR__.'/../../').'/does/not/exist/FileAnalyzer.php'));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
|
||||
}
|
||||
|
||||
public function testIgnoreSymlinkedProjectDirectory(): void
|
||||
@ -199,7 +199,7 @@ class ConfigTest extends TestCase
|
||||
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('tests/AnnotationTest.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('tests/fixtures/symlinktest/a/ignoreme.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
|
||||
|
||||
$regex = '/^unlink\([^\)]+\): (?:Permission denied|No such file or directory)$/';
|
||||
$last_error = error_get_last();
|
||||
@ -247,7 +247,7 @@ class ConfigTest extends TestCase
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
|
||||
}
|
||||
|
||||
public function testIgnoreWildcardFiles(): void
|
||||
@ -272,7 +272,7 @@ class ConfigTest extends TestCase
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php')));
|
||||
}
|
||||
|
||||
public function testIgnoreWildcardFilesInWildcardFolder(): void
|
||||
@ -328,7 +328,6 @@ class ConfigTest extends TestCase
|
||||
$this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php')));
|
||||
$this->assertFalse($config->isInProjectDirs(realpath('examples/StringAnalyzer.php')));
|
||||
}
|
||||
|
||||
public function testIssueHandler(): void
|
||||
@ -352,10 +351,38 @@ class ConfigTest extends TestCase
|
||||
|
||||
$config = $this->project_analyzer->getConfig();
|
||||
|
||||
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('tests/ConfigTest.php')));
|
||||
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath(__FILE__)));
|
||||
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php')));
|
||||
}
|
||||
|
||||
public function testMultipleIssueHandlers(): void
|
||||
{
|
||||
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
|
||||
Config::loadFromXML(
|
||||
dirname(__DIR__, 2),
|
||||
'<?xml version="1.0"?>
|
||||
<psalm>
|
||||
<projectFiles>
|
||||
<directory name="src" />
|
||||
<directory name="tests" />
|
||||
</projectFiles>
|
||||
|
||||
<issueHandlers>
|
||||
<MissingReturnType errorLevel="suppress" />
|
||||
</issueHandlers>
|
||||
<issueHandlers>
|
||||
<UndefinedClass errorLevel="suppress" />
|
||||
</issueHandlers>
|
||||
</psalm>'
|
||||
)
|
||||
);
|
||||
|
||||
$config = $this->project_analyzer->getConfig();
|
||||
|
||||
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath(__FILE__)));
|
||||
$this->assertFalse($config->reportIssueInFile('UndefinedClass', realpath(__FILE__)));
|
||||
}
|
||||
|
||||
public function testIssueHandlerWithCustomErrorLevels(): void
|
||||
{
|
||||
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
|
||||
|
@ -18,26 +18,35 @@ use Psalm\Internal\RuntimeCaches;
|
||||
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_diff;
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function explode;
|
||||
use function file;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function glob;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function scandir;
|
||||
use function sort;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
use function trim;
|
||||
use function usort;
|
||||
use function var_export;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const FILE_IGNORE_NEW_LINES;
|
||||
use const FILE_SKIP_EMPTY_LINES;
|
||||
use const LIBXML_NONET;
|
||||
|
||||
class DocumentationTest extends TestCase
|
||||
@ -90,7 +99,7 @@ class DocumentationTest extends TestCase
|
||||
*/
|
||||
private static function getCodeBlocksFromDocs(): array
|
||||
{
|
||||
$issues_dir = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'docs' . DIRECTORY_SEPARATOR . 'running_psalm' . DIRECTORY_SEPARATOR . 'issues';
|
||||
$issues_dir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'docs' . DIRECTORY_SEPARATOR . 'running_psalm' . DIRECTORY_SEPARATOR . 'issues';
|
||||
|
||||
if (!file_exists($issues_dir)) {
|
||||
throw new UnexpectedValueException('docs not found');
|
||||
@ -420,4 +429,52 @@ class DocumentationTest extends TestCase
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that issues.md contains the expected links to issue documentation.
|
||||
* issues.md can be generated automatically with bin/generate_documentation_issues_list.php.
|
||||
*/
|
||||
public function testIssuesIndex(): void
|
||||
{
|
||||
$docs_dir = dirname(__DIR__) . DIRECTORY_SEPARATOR . "docs" . DIRECTORY_SEPARATOR . "running_psalm" . DIRECTORY_SEPARATOR;
|
||||
$issues_index = "{$docs_dir}issues.md";
|
||||
$issues_dir = "{$docs_dir}issues";
|
||||
|
||||
if (!file_exists($issues_dir)) {
|
||||
throw new UnexpectedValueException("Issues documentation not found");
|
||||
}
|
||||
|
||||
if (!file_exists($issues_index)) {
|
||||
throw new UnexpectedValueException("Issues index not found");
|
||||
}
|
||||
|
||||
$issues_index_contents = file($issues_index, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
|
||||
array_shift($issues_index_contents); // Remove title
|
||||
|
||||
$issues_index_list = array_map(function (string $issues_line) {
|
||||
preg_match('/^ - \[([^\]]*)\]\(issues\/\1\.md\)$/', $issues_line, $matches);
|
||||
$this->assertCount(2, $matches, "Invalid format in issues index: $issues_line");
|
||||
return $matches[1];
|
||||
}, $issues_index_contents);
|
||||
|
||||
$issue_files = array_filter(array_map(function (string $issue_file) {
|
||||
if ($issue_file === "." || $issue_file === "..") {
|
||||
return false;
|
||||
}
|
||||
$this->assertStringEndsWith(".md", $issue_file, "Invalid file in issues documentation: $issue_file");
|
||||
return substr($issue_file, 0, strlen($issue_file) - 3);
|
||||
}, scandir($issues_dir)));
|
||||
|
||||
$unlisted_issues = array_diff($issue_files, $issues_index_list);
|
||||
$this->assertEmpty($unlisted_issues, "Issue documentation missing from issues.md: " . implode(", ", $unlisted_issues));
|
||||
|
||||
$missing_documentation = array_diff($issues_index_list, $issue_files);
|
||||
$this->assertEmpty($missing_documentation, "issues.md has link to non-existent documentation for: " . implode(", ", $missing_documentation));
|
||||
|
||||
$sorted = $issues_index_list;
|
||||
usort($sorted, "strcasecmp");
|
||||
for ($i = 0; $i < count($sorted); ++$i) {
|
||||
$this->assertEquals($sorted[$i], $issues_index_list[$i], "issues.md out of order, expected {$sorted[$i]} before {$issues_index_list[$i]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1054,9 +1054,9 @@ class FunctionCallTest extends TestCase
|
||||
$d = hrtime(false);',
|
||||
'assertions' => [
|
||||
'$a' => 'int',
|
||||
'$b' => 'array{0: int, 1: int}',
|
||||
'$c' => 'array{0: int, 1: int}|int',
|
||||
'$d' => 'array{0: int, 1: int}',
|
||||
'$b' => 'array{int, int}',
|
||||
'$c' => 'array{int, int}|int',
|
||||
'$d' => 'array{int, int}',
|
||||
],
|
||||
],
|
||||
'hrtimeCanBeFloat' => [
|
||||
@ -2160,7 +2160,7 @@ class FunctionCallTest extends TestCase
|
||||
'tooFewArgsAccurateCount' => [
|
||||
'code' => '<?php
|
||||
preg_match(\'/adsf/\');',
|
||||
'error_message' => 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:21 - Too few arguments for preg_match - expecting 2 but saw 1',
|
||||
'error_message' => 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:21 - Too few arguments for preg_match - expecting subject to be passed',
|
||||
],
|
||||
'compactUndefinedVariable' => [
|
||||
'code' => '<?php
|
||||
|
@ -686,6 +686,9 @@ class IntRangeTest extends TestCase
|
||||
|
||||
assert($length === 1);
|
||||
',
|
||||
'assertions' => [
|
||||
'$length===' => '1',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use Psalm\Tests\TestCase;
|
||||
class InternalCallMapHandlerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @covers InternalCallMapHandler::getCallMap
|
||||
* @covers \Psalm\Internal\Codebase\InternalCallMapHandler::getCallMap
|
||||
*/
|
||||
public function testGetcallmapReturnsAValidCallmap(): void
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\CodeException;
|
||||
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
||||
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
@ -1159,4 +1160,28 @@ class MagicPropertyTest extends TestCase
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSealAllMethodsWithoutFoo(): void
|
||||
{
|
||||
Config::getInstance()->seal_all_properties = true;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
class A {
|
||||
public function __get(string $name) {}
|
||||
}
|
||||
|
||||
class B extends A {}
|
||||
|
||||
$b = new B();
|
||||
$result = $b->foo;
|
||||
'
|
||||
);
|
||||
|
||||
$error_message = 'UndefinedMagicPropertyFetch';
|
||||
$this->expectException(CodeException::class);
|
||||
$this->expectExceptionMessage($error_message);
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +247,27 @@ class ReturnTypeTest extends TestCase
|
||||
'$bees' => 'array<int, B>',
|
||||
],
|
||||
],
|
||||
'extendsStaticConstReturnType' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var int */
|
||||
private const FOO = 1;
|
||||
|
||||
/** @return static::FOO */
|
||||
public function getFoo() {
|
||||
return self::FOO;
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
/** @var int */
|
||||
private const FOO = 2;
|
||||
|
||||
public function getFoo() {
|
||||
return self::FOO;
|
||||
}
|
||||
}',
|
||||
],
|
||||
'issetReturnType' => [
|
||||
'code' => '<?php
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user