1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +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_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'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::createFromRules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'],
'Transliterator::createInverse' => ['Transliterator'],

View File

@ -15941,7 +15941,7 @@ return [
'trader_wclprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'],
'trader_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'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_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'],
'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
class A implements JsonSerializable {
public function jsonSerialize() {
return ['type' => 'A'];

View File

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

View File

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

View File

@ -886,14 +886,7 @@ class MethodComparator
$implementer_signature_return_type,
$guide_signature_return_type
)
: (!$implementer_signature_return_type
&& $guide_signature_return_type->isMixed()
? false
: UnionTypeComparator::isContainedByInPhp(
$implementer_signature_return_type,
$guide_signature_return_type
)
);
: UnionTypeComparator::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type);
if (!$is_contained_by) {
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\AfterEveryFunctionCallAnalysisEvent;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Storage\FunctionStorage;
use Psalm\Storage\Possibilities;
use Psalm\Type;
use Psalm\Type\Atomic;
@ -487,6 +488,8 @@ class FunctionCallAnalyzer extends CallAnalyzer
$is_maybe_root_function = !$function_name instanceof PhpParser\Node\Name\FullyQualified
&& count($function_name->parts) === 1;
$args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs();
if (!$function_call_info->in_call_map) {
$predefined_functions = $codebase->config->getPredefinedFunctions();
$is_predefined = isset($predefined_functions[strtolower($original_function_id)])
@ -498,11 +501,10 @@ class FunctionCallAnalyzer extends CallAnalyzer
$function_call_info->function_id,
$code_location,
$is_maybe_root_function
) === false
) {
if (ArgumentsAnalyzer::analyze(
) === false) {
if ($args && ArgumentsAnalyzer::analyze(
$statements_analyzer,
$stmt->getArgs(),
$args,
null,
null,
true,
@ -662,14 +664,23 @@ class FunctionCallAnalyzer extends CallAnalyzer
}
if ($var_type_part instanceof TClosure || $var_type_part instanceof TCallable) {
if (!$var_type_part->is_pure && ($context->pure || $context->mutation_free)) {
IssueBuffer::maybeAdd(
new ImpureFunctionCall(
'Cannot call an impure function from a mutation-free context',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
);
if (!$var_type_part->is_pure) {
if ($context->pure || $context->mutation_free) {
IssueBuffer::maybeAdd(
new ImpureFunctionCall(
'Cannot call an impure function from a mutation-free context',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
);
}
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;

View File

@ -489,14 +489,15 @@ class AtomicStaticCallAnalyzer
$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(
$statements_analyzer,
$stmt,
$stmt_name,
$context,
'tmp_mixin_var',
$mixin_context,
'__tmp_mixin_var__',
true
);
}
@ -700,18 +701,21 @@ class AtomicStaticCallAnalyzer
// 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
$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(
$statements_analyzer,
$stmt,
$stmt_name,
$context,
'tmp_parent_var'
$tmp_context,
'__tmp_parent_var__'
) === false) {
return false;
}
unset($tmp_context);
// Resolve actual static return type according to caller (i.e. $this) static type
if (isset($context->vars_in_scope['$this'])
&& $method_call_type = $statements_analyzer->node_data->getType($stmt)

View File

@ -226,6 +226,7 @@ class CastAnalyzer
if ($type instanceof Scalar) {
$keyed_array = new TKeyedArray([new Union([$type])]);
$keyed_array->is_list = true;
$keyed_array->sealed = true;
$permissible_atomic_types[] = $keyed_array;
} elseif ($type instanceof TNull) {
$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\Union;
use function dirname;
/**
* @internal
*/
@ -87,10 +89,16 @@ class MagicConstAnalyzer
} else {
$statements_analyzer->node_data->setType($stmt, new Union([new TCallableString]));
}
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File
|| $stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir
) {
$statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()]));
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir) {
$statements_analyzer->node_data->setType(
$stmt,
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_) {
if ($statements_analyzer->getSource() instanceof TraitAnalyzer) {
$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;
}

View File

@ -694,7 +694,7 @@ final class IssueBuffer
: $error_count . ' errors'
) . ' found' . "\n";
} else {
self::printSuccessMessage();
self::printSuccessMessage($project_analyzer);
}
$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
$message = "No errors found!";
@ -811,9 +815,15 @@ final class IssueBuffer
// text style, 1 = bold
$style = "1";
echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n";
echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\e[0m" . "\n";
echo "\e[{$background};{$style}m{$paddingBottom}\e[0m" . "\n";
if ($project_analyzer->stdout_report_options->use_color) {
echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n";
echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\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;
}
class ReflectionIntersectionType extends ReflectionType {
/**
* @return non-empty-list<ReflectionType>
*/
public function getTypes() {}
}
}
namespace FTP {

View File

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

View File

@ -2,9 +2,14 @@
namespace Psalm\Tests;
use Psalm\Context;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use function getcwd;
use const DIRECTORY_SEPARATOR;
class ConstantTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
@ -42,6 +47,42 @@ class ConstantTest extends TestCase
// $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}>
*/

View File

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

View File

@ -211,6 +211,19 @@ class PureAnnotationAdditionTest extends FileManipulationTestCase
'issues_to_fix' => ['MissingPureAnnotation'],
'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
{
$project_analyzer = $this->createMock(ProjectAnalyzer::class);
$project_analyzer->stdout_report_options = new ReportOptions;
ob_start();
IssueBuffer::printSuccessMessage();
IssueBuffer::printSuccessMessage($project_analyzer);
$output = ob_get_clean();
$this->assertStringContainsString('No errors found!', $output);

View File

@ -934,7 +934,7 @@ class MagicMethodAnnotationTest extends TestCase
/** @var D<B> $d */
$d = new D();
$e = $d->get();',
[
'assertions' => [
'$b' => 'B',
'$c' => 'B',
'$e' => 'B',
@ -1120,6 +1120,29 @@ class MagicMethodAnnotationTest extends TestCase
class C {}',
'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'
],
'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'
],
];
}
}