1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Merge branch '4.x' into upstream-master

This commit is contained in:
Bruce Weirdan 2022-02-11 03:51:48 +02:00
commit 11e60fa261
No known key found for this signature in database
GPG Key ID: CFC3AAB181751B0D
20 changed files with 206 additions and 46 deletions

View File

@ -14855,7 +14855,7 @@ return [
'trader_wclprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], 'trader_wclprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'],
'trader_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], 'trader_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'],
'trader_wma' => ['array', 'real'=>'array', 'timePeriod='=>'int'], 'trader_wma' => ['array', 'real'=>'array', 'timePeriod='=>'int'],
'trait_exists' => ['?bool', 'trait'=>'string', 'autoload='=>'bool'], 'trait_exists' => ['bool', 'trait'=>'string', 'autoload='=>'bool'],
'Transliterator::create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'Transliterator::create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'],
'Transliterator::createFromRules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], 'Transliterator::createFromRules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'],
'Transliterator::createInverse' => ['Transliterator'], 'Transliterator::createInverse' => ['Transliterator'],

View File

@ -15941,7 +15941,7 @@ return [
'trader_wclprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], 'trader_wclprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'],
'trader_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], 'trader_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'],
'trader_wma' => ['array', 'real'=>'array', 'timePeriod='=>'int'], 'trader_wma' => ['array', 'real'=>'array', 'timePeriod='=>'int'],
'trait_exists' => ['?bool', 'trait'=>'string', 'autoload='=>'bool'], 'trait_exists' => ['bool', 'trait'=>'string', 'autoload='=>'bool'],
'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'],
'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'],
'transliterator_create_inverse' => ['Transliterator', 'transliterator'=>'Transliterator'], 'transliterator_create_inverse' => ['Transliterator', 'transliterator'=>'Transliterator'],

View File

@ -8,7 +8,6 @@ This issue is emitted when a method overriding a native method is defined withou
```php ```php
<?php <?php
class A implements JsonSerializable { class A implements JsonSerializable {
public function jsonSerialize() { public function jsonSerialize() {
return ['type' => 'A']; return ['type' => 'A'];

View File

@ -17,6 +17,9 @@
<code>$matches[0]</code> <code>$matches[0]</code>
<code>$symbol_parts[1]</code> <code>$symbol_parts[1]</code>
</PossiblyUndefinedIntArrayOffset> </PossiblyUndefinedIntArrayOffset>
<PossiblyUnusedProperty occurrences="1">
<code>$analysis_php_version_id</code>
</PossiblyUnusedProperty>
</file> </file>
<file src="src/Psalm/Config/FileFilter.php"> <file src="src/Psalm/Config/FileFilter.php">
<PossiblyUndefinedIntArrayOffset occurrences="1"> <PossiblyUndefinedIntArrayOffset occurrences="1">
@ -317,6 +320,11 @@
<code>array_keys($template_type_map[$template_param_name])[0]</code> <code>array_keys($template_type_map[$template_param_name])[0]</code>
</PossiblyUndefinedIntArrayOffset> </PossiblyUndefinedIntArrayOffset>
</file> </file>
<file src="src/Psalm/Issue/MethodSignatureMustProvideReturnType.php">
<UnusedClass occurrences="1">
<code>MethodSignatureMustProvideReturnType</code>
</UnusedClass>
</file>
<file src="src/Psalm/Node/Stmt/VirtualClass.php"> <file src="src/Psalm/Node/Stmt/VirtualClass.php">
<PropertyNotSetInConstructor occurrences="1"> <PropertyNotSetInConstructor occurrences="1">
<code>VirtualClass</code> <code>VirtualClass</code>

View File

@ -177,8 +177,7 @@ class FileFilter
} }
throw new ConfigException( throw new ConfigException(
'Could not resolve config path to ' . $base_dir 'Could not resolve config path to ' . $prospective_directory_path
. DIRECTORY_SEPARATOR . $directory_path
); );
} }

View File

@ -886,14 +886,7 @@ class MethodComparator
$implementer_signature_return_type, $implementer_signature_return_type,
$guide_signature_return_type $guide_signature_return_type
) )
: (!$implementer_signature_return_type : UnionTypeComparator::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type);
&& $guide_signature_return_type->isMixed()
? false
: UnionTypeComparator::isContainedByInPhp(
$implementer_signature_return_type,
$guide_signature_return_type
)
);
if (!$is_contained_by) { if (!$is_contained_by) {
if ($codebase->analysis_php_version_id >= 8_00_00 if ($codebase->analysis_php_version_id >= 8_00_00

View File

@ -38,6 +38,7 @@ use Psalm\Node\VirtualIdentifier;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Plugin\EventHandler\Event\AfterEveryFunctionCallAnalysisEvent; use Psalm\Plugin\EventHandler\Event\AfterEveryFunctionCallAnalysisEvent;
use Psalm\Storage\FunctionLikeParameter; use Psalm\Storage\FunctionLikeParameter;
use Psalm\Storage\FunctionStorage;
use Psalm\Storage\Possibilities; use Psalm\Storage\Possibilities;
use Psalm\Type; use Psalm\Type;
use Psalm\Type\Atomic; use Psalm\Type\Atomic;
@ -487,6 +488,8 @@ class FunctionCallAnalyzer extends CallAnalyzer
$is_maybe_root_function = !$function_name instanceof PhpParser\Node\Name\FullyQualified $is_maybe_root_function = !$function_name instanceof PhpParser\Node\Name\FullyQualified
&& count($function_name->parts) === 1; && count($function_name->parts) === 1;
$args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs();
if (!$function_call_info->in_call_map) { if (!$function_call_info->in_call_map) {
$predefined_functions = $codebase->config->getPredefinedFunctions(); $predefined_functions = $codebase->config->getPredefinedFunctions();
$is_predefined = isset($predefined_functions[strtolower($original_function_id)]) $is_predefined = isset($predefined_functions[strtolower($original_function_id)])
@ -498,11 +501,10 @@ class FunctionCallAnalyzer extends CallAnalyzer
$function_call_info->function_id, $function_call_info->function_id,
$code_location, $code_location,
$is_maybe_root_function $is_maybe_root_function
) === false ) === false) {
) { if ($args && ArgumentsAnalyzer::analyze(
if (ArgumentsAnalyzer::analyze(
$statements_analyzer, $statements_analyzer,
$stmt->getArgs(), $args,
null, null,
null, null,
true, true,
@ -662,7 +664,8 @@ class FunctionCallAnalyzer extends CallAnalyzer
} }
if ($var_type_part instanceof TClosure || $var_type_part instanceof TCallable) { if ($var_type_part instanceof TClosure || $var_type_part instanceof TCallable) {
if (!$var_type_part->is_pure && ($context->pure || $context->mutation_free)) { if (!$var_type_part->is_pure) {
if ($context->pure || $context->mutation_free) {
IssueBuffer::maybeAdd( IssueBuffer::maybeAdd(
new ImpureFunctionCall( new ImpureFunctionCall(
'Cannot call an impure function from a mutation-free context', 'Cannot call an impure function from a mutation-free context',
@ -672,6 +675,14 @@ class FunctionCallAnalyzer extends CallAnalyzer
); );
} }
if (!$function_call_info->function_storage) {
$function_call_info->function_storage = new FunctionStorage();
}
$function_call_info->function_storage->pure = false;
$function_call_info->function_storage->mutation_free = false;
}
$function_call_info->function_params = $var_type_part->params; $function_call_info->function_params = $var_type_part->params;
if (($stmt_type = $statements_analyzer->node_data->getType($real_stmt)) if (($stmt_type = $statements_analyzer->node_data->getType($real_stmt))

View File

@ -489,14 +489,15 @@ class AtomicStaticCallAnalyzer
$class_storage->final $class_storage->final
); );
$context->vars_in_scope['$tmp_mixin_var'] = $new_lhs_type; $mixin_context = clone $context;
$mixin_context->vars_in_scope['$__tmp_mixin_var__'] = $new_lhs_type;
return self::forwardCallToInstanceMethod( return self::forwardCallToInstanceMethod(
$statements_analyzer, $statements_analyzer,
$stmt, $stmt,
$stmt_name, $stmt_name,
$context, $mixin_context,
'tmp_mixin_var', '__tmp_mixin_var__',
true true
); );
} }
@ -700,18 +701,21 @@ class AtomicStaticCallAnalyzer
// with nonexistent method, we try to forward to instance method call for resolve pseudo method. // with nonexistent method, we try to forward to instance method call for resolve pseudo method.
// Use parent type as static type for the method call // Use parent type as static type for the method call
$context->vars_in_scope['$tmp_parent_var'] = new Union([$lhs_type_part]); $tmp_context = clone $context;
$tmp_context->vars_in_scope['$__tmp_parent_var__'] = new Union([$lhs_type_part]);
if (self::forwardCallToInstanceMethod( if (self::forwardCallToInstanceMethod(
$statements_analyzer, $statements_analyzer,
$stmt, $stmt,
$stmt_name, $stmt_name,
$context, $tmp_context,
'tmp_parent_var' '__tmp_parent_var__'
) === false) { ) === false) {
return false; return false;
} }
unset($tmp_context);
// Resolve actual static return type according to caller (i.e. $this) static type // Resolve actual static return type according to caller (i.e. $this) static type
if (isset($context->vars_in_scope['$this']) if (isset($context->vars_in_scope['$this'])
&& $method_call_type = $statements_analyzer->node_data->getType($stmt) && $method_call_type = $statements_analyzer->node_data->getType($stmt)

View File

@ -226,6 +226,7 @@ class CastAnalyzer
if ($type instanceof Scalar) { if ($type instanceof Scalar) {
$keyed_array = new TKeyedArray([new Union([$type])]); $keyed_array = new TKeyedArray([new Union([$type])]);
$keyed_array->is_list = true; $keyed_array->is_list = true;
$keyed_array->sealed = true;
$permissible_atomic_types[] = $keyed_array; $permissible_atomic_types[] = $keyed_array;
} elseif ($type instanceof TNull) { } elseif ($type instanceof TNull) {
$permissible_atomic_types[] = new TArray([Type::getNever(), Type::getNever()]); $permissible_atomic_types[] = new TArray([Type::getNever(), Type::getNever()]);

View File

@ -16,6 +16,8 @@ use Psalm\Type\Atomic\TCallableString;
use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Union; use Psalm\Type\Union;
use function dirname;
/** /**
* @internal * @internal
*/ */
@ -87,10 +89,16 @@ class MagicConstAnalyzer
} else { } else {
$statements_analyzer->node_data->setType($stmt, new Union([new TCallableString])); $statements_analyzer->node_data->setType($stmt, new Union([new TCallableString]));
} }
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir) {
|| $stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir $statements_analyzer->node_data->setType(
) { $stmt,
$statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()])); Type::getString(dirname($statements_analyzer->getSource()->getFilePath()))
);
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File) {
$statements_analyzer->node_data->setType(
$stmt,
Type::getString($statements_analyzer->getSource()->getFilePath())
);
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Trait_) { } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Trait_) {
if ($statements_analyzer->getSource() instanceof TraitAnalyzer) { if ($statements_analyzer->getSource() instanceof TraitAnalyzer) {
$statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()])); $statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()]));

View File

@ -433,6 +433,18 @@ class SimpleTypeInferer
} }
} }
if ($stmt instanceof PhpParser\Node\Expr\New_) {
$resolved_class_name = $stmt->class->getAttribute('resolvedName');
if (!is_string($resolved_class_name)) {
return null;
}
return new Union([
new Type\Atomic\TNamedObject($resolved_class_name)
]);
}
return null; return null;
} }

View File

@ -694,7 +694,7 @@ final class IssueBuffer
: $error_count . ' errors' : $error_count . ' errors'
) . ' found' . "\n"; ) . ' found' . "\n";
} else { } else {
self::printSuccessMessage(); self::printSuccessMessage($project_analyzer);
} }
$show_info = $project_analyzer->stdout_report_options->show_info; $show_info = $project_analyzer->stdout_report_options->show_info;
@ -785,8 +785,12 @@ final class IssueBuffer
} }
} }
public static function printSuccessMessage(): void public static function printSuccessMessage(ProjectAnalyzer $project_analyzer): void
{ {
if (!$project_analyzer->stdout_report_options) {
throw new UnexpectedValueException('Cannot print success message without stdout report options');
}
// this message will be printed // this message will be printed
$message = "No errors found!"; $message = "No errors found!";
@ -811,9 +815,15 @@ final class IssueBuffer
// text style, 1 = bold // text style, 1 = bold
$style = "1"; $style = "1";
if ($project_analyzer->stdout_report_options->use_color) {
echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n"; echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n";
echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\e[0m" . "\n"; echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\e[0m" . "\n";
echo "\e[{$background};{$style}m{$paddingBottom}\e[0m" . "\n"; echo "\e[{$background};{$style}m{$paddingBottom}\e[0m" . "\n";
} else {
echo "\n";
echo "$messageWithPadding\n";
echo "\n";
}
} }
/** /**

View File

@ -56,6 +56,13 @@ namespace {
*/ */
public function getBackingValue(): int|string; public function getBackingValue(): int|string;
} }
class ReflectionIntersectionType extends ReflectionType {
/**
* @return non-empty-list<ReflectionType>
*/
public function getTypes() {}
}
} }
namespace FTP { namespace FTP {

View File

@ -798,6 +798,11 @@ class ClosureTest extends TestCase
'ignored_issues' => [], 'ignored_issues' => [],
'php_version' => '8.1' 'php_version' => '8.1'
], ],
'unknownFirstClassCallable' => [
'code' => '<?php
/** @psalm-suppress UndefinedFunction */
unknown(...);',
],
]; ];
} }

View File

@ -2,9 +2,14 @@
namespace Psalm\Tests; namespace Psalm\Tests;
use Psalm\Context;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use function getcwd;
use const DIRECTORY_SEPARATOR;
class ConstantTest extends TestCase class ConstantTest extends TestCase
{ {
use InvalidCodeAnalysisTestTrait; use InvalidCodeAnalysisTestTrait;
@ -42,6 +47,42 @@ class ConstantTest extends TestCase
// $this->analyzeFile($file_path, new Context()); // $this->analyzeFile($file_path, new Context());
// } // }
public function testUseObjectConstant(): void
{
$file1 = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file1.php';
$file2 = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file2.php';
$this->addFile(
$file1,
'<?php
namespace Foo;
final class Bar {}
const bar = new Bar();
'
);
$this->addFile(
$file2,
'<?php
namespace Baz;
use Foo\Bar;
use const Foo\bar;
require("tests/file1.php");
function bar(): Bar
{
return bar;
}
'
);
$this->analyzeFile($file1, new Context());
$this->analyzeFile($file2, new Context());
}
/** /**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>, php_version?: string}> * @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>, php_version?: string}>
*/ */

View File

@ -218,10 +218,6 @@ class DocumentationTest extends TestCase
$this->markTestSkipped(); $this->markTestSkipped();
} }
if (strpos($error_message, 'MethodSignatureMustProvideReturnType') !== false) {
$php_version = '8.1';
}
$this->project_analyzer->setPhpVersion($php_version, 'tests'); $this->project_analyzer->setPhpVersion($php_version, 'tests');
if ($check_references) { if ($check_references) {
@ -290,6 +286,10 @@ class DocumentationTest extends TestCase
case 'TraitMethodSignatureMismatch': case 'TraitMethodSignatureMismatch':
continue 2; continue 2;
/** @todo reinstate this test when the issue is restored */
case 'MethodSignatureMustProvideReturnType':
continue 2;
case 'InvalidFalsableReturnType': case 'InvalidFalsableReturnType':
$ignored_issues = ['FalsableReturnStatement']; $ignored_issues = ['FalsableReturnStatement'];
break; break;

View File

@ -211,6 +211,19 @@ class PureAnnotationAdditionTest extends FileManipulationTestCase
'issues_to_fix' => ['MissingPureAnnotation'], 'issues_to_fix' => ['MissingPureAnnotation'],
'safe_types' => true, 'safe_types' => true,
], ],
'dontAddPureIfCallableNotPure' => [
'input' => '<?php
function pure(callable $callable): string{
return $callable();
}',
'output' => '<?php
function pure(callable $callable): string{
return $callable();
}',
'php_version' => '7.4',
'issues_to_fix' => ['MissingPureAnnotation'],
'safe_types' => true,
],
]; ];
} }
} }

View File

@ -113,8 +113,10 @@ class IssueBufferTest extends TestCase
public function testPrintSuccessMessageWorks(): void public function testPrintSuccessMessageWorks(): void
{ {
$project_analyzer = $this->createMock(ProjectAnalyzer::class);
$project_analyzer->stdout_report_options = new ReportOptions;
ob_start(); ob_start();
IssueBuffer::printSuccessMessage(); IssueBuffer::printSuccessMessage($project_analyzer);
$output = ob_get_clean(); $output = ob_get_clean();
$this->assertStringContainsString('No errors found!', $output); $this->assertStringContainsString('No errors found!', $output);

View File

@ -934,7 +934,7 @@ class MagicMethodAnnotationTest extends TestCase
/** @var D<B> $d */ /** @var D<B> $d */
$d = new D(); $d = new D();
$e = $d->get();', $e = $d->get();',
[ 'assertions' => [
'$b' => 'B', '$b' => 'B',
'$c' => 'B', '$c' => 'B',
'$e' => 'B', '$e' => 'B',
@ -1120,6 +1120,29 @@ class MagicMethodAnnotationTest extends TestCase
class C {}', class C {}',
'error_message' => 'InvalidDocblock', 'error_message' => 'InvalidDocblock',
], ],
'magicParentCallShouldNotPolluteContext' => [
'code' => '<?php
/**
* @method baz(): Foo
*/
class Foo
{
public function __call()
{
return new self();
}
}
class Bar extends Foo
{
public function baz(): Foo
{
parent::baz();
return $__tmp_parent_var__;
}
}',
'error_message' => 'UndefinedVariable',
]
]; ];
} }

View File

@ -690,6 +690,30 @@ class MixinAnnotationTest extends TestCase
}', }',
'error_message' => 'LessSpecificReturnStatement' 'error_message' => 'LessSpecificReturnStatement'
], ],
'mixinStaticCallShouldNotPolluteContext' => [
'code' => '<?php
/**
* @template T
*/
class Foo
{
public function foobar(): void {}
}
/**
* @template T
* @mixin Foo<T>
*/
class Bar
{
public function baz(): self
{
self::foobar();
return $__tmp_mixin_var__;
}
}',
'error_message' => 'UndefinedVariable'
],
]; ];
} }
} }