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:
commit
11e60fa261
@ -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'],
|
||||
|
@ -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'],
|
||||
|
@ -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'];
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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()]);
|
||||
|
@ -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()]));
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,6 +56,13 @@ namespace {
|
||||
*/
|
||||
public function getBackingValue(): int|string;
|
||||
}
|
||||
|
||||
class ReflectionIntersectionType extends ReflectionType {
|
||||
/**
|
||||
* @return non-empty-list<ReflectionType>
|
||||
*/
|
||||
public function getTypes() {}
|
||||
}
|
||||
}
|
||||
|
||||
namespace FTP {
|
||||
|
@ -798,6 +798,11 @@ class ClosureTest extends TestCase
|
||||
'ignored_issues' => [],
|
||||
'php_version' => '8.1'
|
||||
],
|
||||
'unknownFirstClassCallable' => [
|
||||
'code' => '<?php
|
||||
/** @psalm-suppress UndefinedFunction */
|
||||
unknown(...);',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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}>
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user