Composer updates
This commit is contained in:
@@ -169,6 +169,7 @@ class CodeLocation
|
||||
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
|
||||
/** @psalm-suppress ImpureMethodCall */
|
||||
$file_contents = $codebase->getFileContents($this->file_path);
|
||||
|
||||
$file_length = strlen($file_contents);
|
||||
|
||||
458
vendor/vimeo/psalm/src/Psalm/Codebase.php
vendored
458
vendor/vimeo/psalm/src/Psalm/Codebase.php
vendored
@@ -37,6 +37,8 @@ use Psalm\Internal\Codebase\Scanner;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\DataFlow\TaintSink;
|
||||
use Psalm\Internal\DataFlow\TaintSource;
|
||||
use Psalm\Internal\LanguageServer\PHPMarkdownContent;
|
||||
use Psalm\Internal\LanguageServer\Reference;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Provider\ClassLikeStorageProvider;
|
||||
use Psalm\Internal\Provider\FileProvider;
|
||||
@@ -67,8 +69,10 @@ use ReflectionType;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_combine;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function array_reverse;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function error_log;
|
||||
@@ -82,6 +86,7 @@ use function krsort;
|
||||
use function ksort;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strrpos;
|
||||
@@ -390,15 +395,19 @@ final class Codebase
|
||||
/**
|
||||
* @param array<string> $candidate_files
|
||||
*/
|
||||
public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files): void
|
||||
public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files, bool $force = false): void
|
||||
{
|
||||
$this->loadAnalyzer();
|
||||
|
||||
if ($force) {
|
||||
FileReferenceProvider::clearCache();
|
||||
}
|
||||
|
||||
$this->file_reference_provider->loadReferenceCache(false);
|
||||
|
||||
FunctionLikeAnalyzer::clearCache();
|
||||
|
||||
if (!$this->statements_provider->parser_cache_provider) {
|
||||
if ($force || !$this->statements_provider->parser_cache_provider) {
|
||||
$diff_files = $candidate_files;
|
||||
} else {
|
||||
$diff_files = [];
|
||||
@@ -500,7 +509,6 @@ final class Codebase
|
||||
}
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFileContents(string $file_path): string
|
||||
{
|
||||
return $this->file_provider->getContents($file_path);
|
||||
@@ -593,6 +601,7 @@ final class Codebase
|
||||
*/
|
||||
public function findReferencesToProperty(string $property_id): array
|
||||
{
|
||||
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
|
||||
[$fq_class_name, $property_name] = explode('::', $property_id);
|
||||
|
||||
return $this->file_reference_provider->getClassPropertyLocations(
|
||||
@@ -970,7 +979,225 @@ final class Codebase
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ type: string, description?: string|null}|null
|
||||
* Get Markup content from Reference
|
||||
*/
|
||||
public function getMarkupContentForSymbolByReference(
|
||||
Reference $reference
|
||||
): ?PHPMarkdownContent {
|
||||
//Direct Assignment
|
||||
if (is_numeric($reference->symbol[0])) {
|
||||
return new PHPMarkdownContent(
|
||||
preg_replace(
|
||||
'/^[^:]*:/',
|
||||
'',
|
||||
$reference->symbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Class
|
||||
if (strpos($reference->symbol, '::')) {
|
||||
//Class Method
|
||||
if (strpos($reference->symbol, '()')) {
|
||||
$symbol = substr($reference->symbol, 0, -2);
|
||||
|
||||
/** @psalm-suppress ArgumentTypeCoercion */
|
||||
$method_id = new MethodIdentifier(...explode('::', $symbol));
|
||||
|
||||
$declaring_method_id = $this->methods->getDeclaringMethodId(
|
||||
$method_id,
|
||||
);
|
||||
|
||||
if (!$declaring_method_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$storage = $this->methods->getStorage($declaring_method_id);
|
||||
|
||||
return new PHPMarkdownContent(
|
||||
$storage->getHoverMarkdown(),
|
||||
"{$storage->defining_fqcln}::{$storage->cased_name}",
|
||||
$storage->description,
|
||||
);
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
|
||||
[, $symbol_name] = explode('::', $reference->symbol);
|
||||
|
||||
//Class Property
|
||||
if (strpos($reference->symbol, '$') !== false) {
|
||||
$property_id = preg_replace('/^\\\\/', '', $reference->symbol);
|
||||
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
|
||||
[$fq_class_name, $property_name] = explode('::$', $property_id);
|
||||
$class_storage = $this->classlikes->getStorageFor($fq_class_name);
|
||||
|
||||
//Get Real Properties
|
||||
if (isset($class_storage->declaring_property_ids[$property_name])) {
|
||||
$declaring_property_class = $class_storage->declaring_property_ids[$property_name];
|
||||
$declaring_class_storage = $this->classlike_storage_provider->get($declaring_property_class);
|
||||
|
||||
if (isset($declaring_class_storage->properties[$property_name])) {
|
||||
$storage = $declaring_class_storage->properties[$property_name];
|
||||
return new PHPMarkdownContent(
|
||||
"{$storage->getInfo()} {$symbol_name}",
|
||||
$reference->symbol,
|
||||
$storage->description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Get Docblock properties
|
||||
if (isset($class_storage->pseudo_property_set_types['$'.$property_name])) {
|
||||
return new PHPMarkdownContent(
|
||||
'public '.
|
||||
(string) $class_storage->pseudo_property_set_types['$'.$property_name].' $'.$property_name,
|
||||
$reference->symbol,
|
||||
);
|
||||
}
|
||||
|
||||
//Get Docblock properties
|
||||
if (isset($class_storage->pseudo_property_get_types['$'.$property_name])) {
|
||||
return new PHPMarkdownContent(
|
||||
'public '.
|
||||
(string) $class_storage->pseudo_property_get_types['$'.$property_name].' $'.$property_name,
|
||||
$reference->symbol,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
|
||||
[$fq_classlike_name, $const_name] = explode(
|
||||
'::',
|
||||
$reference->symbol,
|
||||
);
|
||||
|
||||
$class_constants = $this->classlikes->getConstantsForClass(
|
||||
$fq_classlike_name,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
);
|
||||
|
||||
if (!isset($class_constants[$const_name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Class Constant
|
||||
return new PHPMarkdownContent(
|
||||
$class_constants[$const_name]->getHoverMarkdown($const_name),
|
||||
$fq_classlike_name . '::' . $const_name,
|
||||
$class_constants[$const_name]->description,
|
||||
);
|
||||
}
|
||||
|
||||
//Procedural Function
|
||||
if (strpos($reference->symbol, '()')) {
|
||||
$function_id = strtolower(substr($reference->symbol, 0, -2));
|
||||
$file_storage = $this->file_storage_provider->get(
|
||||
$reference->file_path,
|
||||
);
|
||||
|
||||
if (isset($file_storage->functions[$function_id])) {
|
||||
$function_storage = $file_storage->functions[$function_id];
|
||||
|
||||
return new PHPMarkdownContent(
|
||||
$function_storage->getHoverMarkdown(),
|
||||
$function_id,
|
||||
$function_storage->description,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$function_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$function = $this->functions->getStorage(null, $function_id);
|
||||
|
||||
return new PHPMarkdownContent(
|
||||
$function->getHoverMarkdown(),
|
||||
$function_id,
|
||||
$function->description,
|
||||
);
|
||||
}
|
||||
|
||||
//Procedural Variable
|
||||
if (strpos($reference->symbol, '$') === 0) {
|
||||
$type = VariableFetchAnalyzer::getGlobalType($reference->symbol, $this->analysis_php_version_id);
|
||||
if (!$type->isMixed()) {
|
||||
return new PHPMarkdownContent(
|
||||
(string) $type,
|
||||
$reference->symbol,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$storage = $this->classlike_storage_provider->get(
|
||||
$reference->symbol,
|
||||
);
|
||||
return new PHPMarkdownContent(
|
||||
($storage->abstract ? 'abstract ' : '') .
|
||||
'class ' .
|
||||
$storage->name,
|
||||
$storage->name,
|
||||
$storage->description,
|
||||
);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
//continue on as normal
|
||||
}
|
||||
|
||||
if (strpos($reference->symbol, '\\')) {
|
||||
$const_name_parts = explode('\\', $reference->symbol);
|
||||
$const_name = array_pop($const_name_parts);
|
||||
$namespace_name = implode('\\', $const_name_parts);
|
||||
|
||||
$namespace_constants = NamespaceAnalyzer::getConstantsForNamespace(
|
||||
$namespace_name,
|
||||
ReflectionProperty::IS_PUBLIC,
|
||||
);
|
||||
//Namespace Constant
|
||||
if (isset($namespace_constants[$const_name])) {
|
||||
$type = $namespace_constants[$const_name];
|
||||
return new PHPMarkdownContent(
|
||||
$reference->symbol . ' ' . $type,
|
||||
$reference->symbol,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$file_storage = $this->file_storage_provider->get(
|
||||
$reference->file_path,
|
||||
);
|
||||
// ?
|
||||
if (isset($file_storage->constants[$reference->symbol])) {
|
||||
return new PHPMarkdownContent(
|
||||
'const ' .
|
||||
$reference->symbol .
|
||||
' ' .
|
||||
$file_storage->constants[$reference->symbol],
|
||||
$reference->symbol,
|
||||
);
|
||||
}
|
||||
$type = ConstFetchAnalyzer::getGlobalConstType(
|
||||
$this,
|
||||
$reference->symbol,
|
||||
$reference->symbol,
|
||||
);
|
||||
|
||||
//Global Constant
|
||||
if ($type) {
|
||||
return new PHPMarkdownContent(
|
||||
'const ' . $reference->symbol . ' ' . $type,
|
||||
$reference->symbol,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new PHPMarkdownContent($reference->symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
* @deprecated will be removed in Psalm 6. use {@see Codebase::getSymbolLocationByReference()} instead
|
||||
*/
|
||||
public function getSymbolInformation(string $file_path, string $symbol): ?array
|
||||
{
|
||||
@@ -995,7 +1222,7 @@ final class Codebase
|
||||
$storage = $this->methods->getStorage($declaring_method_id);
|
||||
|
||||
return [
|
||||
'type' => '<?php ' . $storage->getSignature(true),
|
||||
'type' => '<?php ' . $storage->getCompletionSignature(),
|
||||
'description' => $storage->description,
|
||||
];
|
||||
}
|
||||
@@ -1036,7 +1263,7 @@ final class Codebase
|
||||
$function_storage = $file_storage->functions[$function_id];
|
||||
|
||||
return [
|
||||
'type' => '<?php ' . $function_storage->getSignature(true),
|
||||
'type' => '<?php ' . $function_storage->getCompletionSignature(),
|
||||
'description' => $function_storage->description,
|
||||
];
|
||||
}
|
||||
@@ -1047,7 +1274,7 @@ final class Codebase
|
||||
|
||||
$function = $this->functions->getStorage(null, $function_id);
|
||||
return [
|
||||
'type' => '<?php ' . $function->getSignature(true),
|
||||
'type' => '<?php ' . $function->getCompletionSignature(),
|
||||
'description' => $function->description,
|
||||
];
|
||||
}
|
||||
@@ -1100,6 +1327,10 @@ final class Codebase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
* @deprecated will be removed in Psalm 6. use {@see Codebase::getSymbolLocationByReference()} instead
|
||||
*/
|
||||
public function getSymbolLocation(string $file_path, string $symbol): ?CodeLocation
|
||||
{
|
||||
if (is_numeric($symbol[0])) {
|
||||
@@ -1182,11 +1413,127 @@ final class Codebase
|
||||
}
|
||||
}
|
||||
|
||||
public function getSymbolLocationByReference(Reference $reference): ?CodeLocation
|
||||
{
|
||||
if (is_numeric($reference->symbol[0])) {
|
||||
$symbol = preg_replace('/:.*/', '', $reference->symbol);
|
||||
$symbol_parts = explode('-', $symbol);
|
||||
|
||||
if (!isset($symbol_parts[0]) || !isset($symbol_parts[1])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file_contents = $this->getFileContents($reference->file_path);
|
||||
|
||||
return new Raw(
|
||||
$file_contents,
|
||||
$reference->file_path,
|
||||
$this->config->shortenFileName($reference->file_path),
|
||||
(int) $symbol_parts[0],
|
||||
(int) $symbol_parts[1],
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (strpos($reference->symbol, '::')) {
|
||||
if (strpos($reference->symbol, '()')) {
|
||||
$symbol = substr($reference->symbol, 0, -2);
|
||||
|
||||
/** @psalm-suppress ArgumentTypeCoercion */
|
||||
$method_id = new MethodIdentifier(
|
||||
...explode('::', $symbol),
|
||||
);
|
||||
|
||||
$declaring_method_id = $this->methods->getDeclaringMethodId(
|
||||
$method_id,
|
||||
);
|
||||
|
||||
if (!$declaring_method_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$storage = $this->methods->getStorage($declaring_method_id);
|
||||
|
||||
return $storage->location;
|
||||
}
|
||||
|
||||
if (strpos($reference->symbol, '$') !== false) {
|
||||
$storage = $this->properties->getStorage(
|
||||
$reference->symbol,
|
||||
);
|
||||
|
||||
return $storage->location;
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
|
||||
[$fq_classlike_name, $const_name] = explode(
|
||||
'::',
|
||||
$reference->symbol,
|
||||
);
|
||||
|
||||
$class_constants = $this->classlikes->getConstantsForClass(
|
||||
$fq_classlike_name,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
);
|
||||
|
||||
if (!isset($class_constants[$const_name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $class_constants[$const_name]->location;
|
||||
}
|
||||
|
||||
if (strpos($reference->symbol, '()')) {
|
||||
$file_storage = $this->file_storage_provider->get(
|
||||
$reference->file_path,
|
||||
);
|
||||
|
||||
$function_id = strtolower(substr($reference->symbol, 0, -2));
|
||||
|
||||
if (isset($file_storage->functions[$function_id])) {
|
||||
return $file_storage->functions[$function_id]->location;
|
||||
}
|
||||
|
||||
if (!$function_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->functions->getStorage(null, $function_id)
|
||||
->location;
|
||||
}
|
||||
|
||||
return $this->classlike_storage_provider->get(
|
||||
$reference->symbol,
|
||||
)->location;
|
||||
} catch (UnexpectedValueException $e) {
|
||||
error_log($e->getMessage());
|
||||
|
||||
return null;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
* @return array{0: string, 1: Range}|null
|
||||
*/
|
||||
public function getReferenceAtPosition(string $file_path, Position $position): ?array
|
||||
{
|
||||
$ref = $this->getReferenceAtPositionAsReference($file_path, $position);
|
||||
if ($ref === null) {
|
||||
return null;
|
||||
}
|
||||
return [$ref->symbol, $ref->range];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Reference from Position
|
||||
*/
|
||||
public function getReferenceAtPositionAsReference(
|
||||
string $file_path,
|
||||
Position $position
|
||||
): ?Reference {
|
||||
$is_open = $this->file_provider->isOpen($file_path);
|
||||
|
||||
if (!$is_open) {
|
||||
@@ -1197,33 +1544,37 @@ final class Codebase
|
||||
|
||||
$offset = $position->toOffset($file_contents);
|
||||
|
||||
[$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path);
|
||||
|
||||
$reference = null;
|
||||
|
||||
if (!$reference_map && !$type_map) {
|
||||
return null;
|
||||
}
|
||||
$reference_maps = $this->analyzer->getMapsForFile($file_path);
|
||||
|
||||
$reference_start_pos = null;
|
||||
$reference_end_pos = null;
|
||||
$symbol = null;
|
||||
|
||||
ksort($reference_map);
|
||||
foreach ($reference_maps as $reference_map) {
|
||||
ksort($reference_map);
|
||||
|
||||
foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) {
|
||||
if ($offset < $start_pos) {
|
||||
foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) {
|
||||
if ($offset < $start_pos) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($offset > $end_pos) {
|
||||
continue;
|
||||
}
|
||||
$reference_start_pos = $start_pos;
|
||||
$reference_end_pos = $end_pos;
|
||||
$symbol = $possible_reference;
|
||||
}
|
||||
|
||||
if ($symbol !== null &&
|
||||
$reference_start_pos !== null &&
|
||||
$reference_end_pos !== null
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($offset > $end_pos) {
|
||||
continue;
|
||||
}
|
||||
$reference_start_pos = $start_pos;
|
||||
$reference_end_pos = $end_pos;
|
||||
$reference = $possible_reference;
|
||||
}
|
||||
|
||||
if ($reference === null || $reference_start_pos === null || $reference_end_pos === null) {
|
||||
if ($symbol === null || $reference_start_pos === null || $reference_end_pos === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1232,7 +1583,7 @@ final class Codebase
|
||||
self::getPositionFromOffset($reference_end_pos, $file_contents),
|
||||
);
|
||||
|
||||
return [$reference, $range];
|
||||
return new Reference($file_path, $symbol, $range);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1399,6 +1750,7 @@ final class Codebase
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
|
||||
$num_whitespace_bytes = preg_match('/\G\s+/', $file_contents, $matches, 0, $end_pos_excluding_whitespace)
|
||||
? strlen($matches[0])
|
||||
: 0;
|
||||
@@ -1487,8 +1839,11 @@ final class Codebase
|
||||
/**
|
||||
* @return list<CompletionItem>
|
||||
*/
|
||||
public function getCompletionItemsForClassishThing(string $type_string, string $gap): array
|
||||
{
|
||||
public function getCompletionItemsForClassishThing(
|
||||
string $type_string,
|
||||
string $gap,
|
||||
bool $snippets_supported = false
|
||||
): array {
|
||||
$completion_items = [];
|
||||
|
||||
$type = Type::parseString($type_string);
|
||||
@@ -1505,11 +1860,11 @@ final class Codebase
|
||||
$completion_item = new CompletionItem(
|
||||
$method_storage->cased_name,
|
||||
CompletionItemKind::METHOD,
|
||||
(string)$method_storage,
|
||||
$method_storage->getCompletionSignature(),
|
||||
$method_storage->description,
|
||||
(string)$method_storage->visibility,
|
||||
$method_storage->cased_name,
|
||||
$method_storage->cased_name . (count($method_storage->params) !== 0 ? '($0)' : '()'),
|
||||
$method_storage->cased_name,
|
||||
null,
|
||||
null,
|
||||
new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'),
|
||||
@@ -1517,12 +1872,47 @@ final class Codebase
|
||||
2,
|
||||
);
|
||||
|
||||
$completion_item->insertTextFormat = InsertTextFormat::SNIPPET;
|
||||
if ($snippets_supported && count($method_storage->params) > 0) {
|
||||
$completion_item->insertText .= '($0)';
|
||||
$completion_item->insertTextFormat =
|
||||
InsertTextFormat::SNIPPET;
|
||||
} else {
|
||||
$completion_item->insertText .= '()';
|
||||
}
|
||||
|
||||
$completion_items[] = $completion_item;
|
||||
}
|
||||
}
|
||||
|
||||
$pseudo_property_types = [];
|
||||
foreach ($class_storage->pseudo_property_get_types as $property_name => $type) {
|
||||
$pseudo_property_types[$property_name] = new CompletionItem(
|
||||
str_replace('$', '', $property_name),
|
||||
CompletionItemKind::PROPERTY,
|
||||
$type->__toString(),
|
||||
null,
|
||||
'1', //sort text
|
||||
str_replace('$', '', $property_name),
|
||||
($gap === '::' ? '$' : '') .
|
||||
str_replace('$', '', $property_name),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($class_storage->pseudo_property_set_types as $property_name => $type) {
|
||||
$pseudo_property_types[$property_name] = new CompletionItem(
|
||||
str_replace('$', '', $property_name),
|
||||
CompletionItemKind::PROPERTY,
|
||||
$type->__toString(),
|
||||
null,
|
||||
'1',
|
||||
str_replace('$', '', $property_name),
|
||||
($gap === '::' ? '$' : '') .
|
||||
str_replace('$', '', $property_name),
|
||||
);
|
||||
}
|
||||
|
||||
$completion_items = array_merge($completion_items, array_values($pseudo_property_types));
|
||||
|
||||
foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) {
|
||||
$property_storage = $this->properties->getStorage(
|
||||
$declaring_class . '::$' . $property_name,
|
||||
@@ -1715,7 +2105,7 @@ final class Codebase
|
||||
$completion_items[] = new CompletionItem(
|
||||
$function_name,
|
||||
CompletionItemKind::FUNCTION,
|
||||
$function->getSignature(false),
|
||||
$function->getCompletionSignature(),
|
||||
$function->description,
|
||||
null,
|
||||
$function_name,
|
||||
@@ -1832,9 +2222,9 @@ final class Codebase
|
||||
);
|
||||
}
|
||||
|
||||
public function addTemporaryFileChanges(string $file_path, string $new_content): void
|
||||
public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void
|
||||
{
|
||||
$this->file_provider->addTemporaryFileChanges($file_path, $new_content);
|
||||
$this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version);
|
||||
}
|
||||
|
||||
public function removeTemporaryFileChanges(string $file_path): void
|
||||
|
||||
11
vendor/vimeo/psalm/src/Psalm/Config.php
vendored
11
vendor/vimeo/psalm/src/Psalm/Config.php
vendored
@@ -194,6 +194,13 @@ class Config
|
||||
*/
|
||||
public $use_docblock_property_types = false;
|
||||
|
||||
/**
|
||||
* Whether using property annotations in docblocks should implicitly seal properties
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $docblock_property_types_seal_properties = true;
|
||||
|
||||
/**
|
||||
* Whether or not to throw an exception on first error
|
||||
*
|
||||
@@ -1049,6 +1056,7 @@ class Config
|
||||
$booleanAttributes = [
|
||||
'useDocblockTypes' => 'use_docblock_types',
|
||||
'useDocblockPropertyTypes' => 'use_docblock_property_types',
|
||||
'docblockPropertyTypesSealProperties' => 'docblock_property_types_seal_properties',
|
||||
'throwExceptionOnError' => 'throw_exception',
|
||||
'hideExternalErrors' => 'hide_external_errors',
|
||||
'hideAllErrorsExceptPassedFiles' => 'hide_all_errors_except_passed_files',
|
||||
@@ -1727,7 +1735,8 @@ class Config
|
||||
|
||||
public function reportIssueInFile(string $issue_type, string $file_path): bool
|
||||
{
|
||||
if (($this->show_mixed_issues === false || $this->level > 2)
|
||||
if ((($this->level < 3 && $this->show_mixed_issues === false)
|
||||
|| ($this->level > 2 && $this->show_mixed_issues !== true))
|
||||
&& in_array($issue_type, self::MIXED_ISSUES, true)
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -122,6 +122,7 @@ class FileFilter
|
||||
$declare_strict_types = (bool) ($directory['useStrictTypes'] ?? false);
|
||||
|
||||
if ($directory_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
|
||||
/** @var non-empty-string */
|
||||
$prospective_directory_path = $directory_path;
|
||||
} else {
|
||||
$prospective_directory_path = $base_dir . DIRECTORY_SEPARATOR . $directory_path;
|
||||
@@ -238,6 +239,7 @@ class FileFilter
|
||||
$file_path = (string) ($file['name'] ?? '');
|
||||
|
||||
if ($file_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
|
||||
/** @var non-empty-string */
|
||||
$prospective_file_path = $file_path;
|
||||
} else {
|
||||
$prospective_file_path = $base_dir . DIRECTORY_SEPARATOR . $file_path;
|
||||
|
||||
3
vendor/vimeo/psalm/src/Psalm/DocComment.php
vendored
3
vendor/vimeo/psalm/src/Psalm/DocComment.php
vendored
@@ -24,6 +24,7 @@ final class DocComment
|
||||
'assert', 'assert-if-true', 'assert-if-false', 'suppress',
|
||||
'ignore-nullable-return', 'override-property-visibility',
|
||||
'override-method-visibility', 'seal-properties', 'seal-methods',
|
||||
'no-seal-properties', 'no-seal-methods',
|
||||
'ignore-falsable-return', 'variadic', 'pure',
|
||||
'ignore-variable-method', 'ignore-variable-property', 'internal',
|
||||
'taint-sink', 'taint-source', 'assert-untainted', 'scope-this',
|
||||
@@ -33,7 +34,7 @@ final class DocComment
|
||||
'taint-unescape', 'self-out', 'consistent-constructor', 'stub-override',
|
||||
'require-extends', 'require-implements', 'param-out', 'ignore-var',
|
||||
'consistent-templates', 'if-this-is', 'this-out', 'check-type', 'check-type-exact',
|
||||
'api',
|
||||
'api', 'inheritors',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,7 +118,7 @@ final class ErrorBaseline
|
||||
|
||||
foreach ($codeSamples as $codeSample) {
|
||||
$files[$fileName][$issueType]['o'] += 1;
|
||||
$files[$fileName][$issueType]['s'][] = trim($codeSample->textContent);
|
||||
$files[$fileName][$issueType]['s'][] = str_replace("\r\n", "\n", trim($codeSample->textContent));
|
||||
}
|
||||
|
||||
// TODO: Remove in Psalm 6
|
||||
|
||||
@@ -362,16 +362,6 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->leftover_stmts) {
|
||||
(new StatementsAnalyzer(
|
||||
$this,
|
||||
new NodeDataProvider(),
|
||||
))->analyze(
|
||||
$this->leftover_stmts,
|
||||
$class_context,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$storage->abstract) {
|
||||
foreach ($storage->declaring_method_ids as $declaring_method_id) {
|
||||
$method_storage = $codebase->methods->getStorage($declaring_method_id);
|
||||
@@ -712,7 +702,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
new OverriddenPropertyAccess(
|
||||
'Property ' . $fq_class_name . '::$' . $property_name
|
||||
. ' has different access level than '
|
||||
. $storage->name . '::$' . $property_name,
|
||||
. $guide_class_name . '::$' . $property_name,
|
||||
$property_storage->location,
|
||||
),
|
||||
);
|
||||
@@ -1781,8 +1771,6 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
$method_context = clone $class_context;
|
||||
|
||||
foreach ($method_context->vars_in_scope as $context_var_id => $context_type) {
|
||||
$method_context->vars_in_scope[$context_var_id] = $context_type;
|
||||
|
||||
if ($context_type->from_property && $stmt->name->name !== '__construct') {
|
||||
$method_context->vars_in_scope[$context_var_id] =
|
||||
$method_context->vars_in_scope[$context_var_id]->setProperties(['initialized' => true]);
|
||||
|
||||
@@ -14,6 +14,7 @@ use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Issue\InaccessibleProperty;
|
||||
use Psalm\Issue\InheritorViolation;
|
||||
use Psalm\Issue\InvalidClass;
|
||||
use Psalm\Issue\InvalidTemplateParam;
|
||||
use Psalm\Issue\MissingDependency;
|
||||
@@ -28,6 +29,7 @@ use Psalm\Plugin\EventHandler\Event\AfterClassLikeExistenceCheckEvent;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
@@ -92,11 +94,6 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer
|
||||
*/
|
||||
protected ?string $parent_fq_class_name = null;
|
||||
|
||||
/**
|
||||
* @var PhpParser\Node\Stmt[]
|
||||
*/
|
||||
protected array $leftover_stmts = [];
|
||||
|
||||
protected ClassLikeStorage $storage;
|
||||
|
||||
public function __construct(PhpParser\Node\Stmt\ClassLike $class, SourceAnalyzer $source, string $fq_class_name)
|
||||
@@ -336,6 +333,23 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$classUnion = new Union([new TNamedObject($fq_class_name)]);
|
||||
foreach ($class_storage->parent_classes + $class_storage->direct_class_interfaces as $parent_class) {
|
||||
$parent_storage = $codebase->classlikes->getStorageFor($parent_class);
|
||||
if ($parent_storage && $parent_storage->inheritors) {
|
||||
if (!UnionTypeComparator::isContainedBy($codebase, $classUnion, $parent_storage->inheritors)) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InheritorViolation(
|
||||
'Class ' . $fq_class_name . ' is not an allowed inheritor of parent class ' . $parent_class,
|
||||
$code_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($class_storage->invalid_dependencies as $dependency_class_name => $_) {
|
||||
// if the implemented/extended class is stubbed, it may not yet have
|
||||
// been hydrated
|
||||
|
||||
@@ -72,6 +72,7 @@ use function count;
|
||||
use function end;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function krsort;
|
||||
use function mb_strpos;
|
||||
use function md5;
|
||||
use function microtime;
|
||||
@@ -80,6 +81,8 @@ use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @template-covariant TFunction as Closure|Function_|ClassMethod|ArrowFunction
|
||||
@@ -721,7 +724,10 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
if ($expected_exception === $possibly_thrown_exception
|
||||
|| (
|
||||
$codebase->classOrInterfaceExists($possibly_thrown_exception)
|
||||
&& $codebase->classExtendsOrImplements($possibly_thrown_exception, $expected_exception)
|
||||
&& (
|
||||
$codebase->interfaceExtends($possibly_thrown_exception, $expected_exception)
|
||||
|| $codebase->classExtendsOrImplements($possibly_thrown_exception, $expected_exception)
|
||||
)
|
||||
)
|
||||
) {
|
||||
$is_expected = true;
|
||||
@@ -870,96 +876,55 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
): void {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$unused_params = [];
|
||||
$unused_params = $this->detectUnusedParameters($statements_analyzer, $storage, $context);
|
||||
|
||||
foreach ($statements_analyzer->getUnusedVarLocations() as [$var_name, $original_location]) {
|
||||
if (!array_key_exists(substr($var_name, 1), $storage->param_lookup)) {
|
||||
continue;
|
||||
}
|
||||
if (!$storage instanceof MethodStorage
|
||||
|| !$storage->cased_name
|
||||
|| $storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
) {
|
||||
$last_unused_argument_position = $this->detectPreviousUnusedArgumentPosition(
|
||||
$storage,
|
||||
count($storage->params) - 1,
|
||||
);
|
||||
|
||||
if (strpos($var_name, '$_') === 0 || (strpos($var_name, '$unused') === 0 && $var_name !== '$unused')) {
|
||||
continue;
|
||||
}
|
||||
// Sort parameters in reverse order so that we can start from the end of parameters
|
||||
krsort($unused_params, SORT_NUMERIC);
|
||||
|
||||
$position = array_search(substr($var_name, 1), array_keys($storage->param_lookup), true);
|
||||
foreach ($unused_params as $unused_param_position => $unused_param_code_location) {
|
||||
$unused_param_var_name = $storage->params[$unused_param_position]->name;
|
||||
$unused_param_message = 'Param ' . $unused_param_var_name . ' is never referenced in this method';
|
||||
|
||||
if ($position === false) {
|
||||
throw new UnexpectedValueException('$position should not be false here');
|
||||
}
|
||||
// Remove the key as we already report the issue
|
||||
unset($unused_params[$unused_param_position]);
|
||||
|
||||
if ($storage->params[$position]->promoted_property) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$did_match_param = false;
|
||||
|
||||
foreach ($this->function->params as $param) {
|
||||
if ($param->var->getAttribute('endFilePos') === $original_location->raw_file_end) {
|
||||
$did_match_param = true;
|
||||
// Do not report unused required parameters
|
||||
if ($unused_param_position !== $last_unused_argument_position) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$did_match_param) {
|
||||
continue;
|
||||
}
|
||||
$last_unused_argument_position = $this->detectPreviousUnusedArgumentPosition(
|
||||
$storage,
|
||||
$unused_param_position - 1,
|
||||
);
|
||||
|
||||
$assignment_node = DataFlowNode::getForAssignment($var_name, $original_location);
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
|
||||
&& $statements_analyzer->data_flow_graph->isVariableUsed($assignment_node)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$storage instanceof MethodStorage
|
||||
|| !$storage->cased_name
|
||||
|| $storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
) {
|
||||
if ($this instanceof ClosureAnalyzer) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new UnusedClosureParam(
|
||||
'Param ' . $var_name . ' is never referenced in this method',
|
||||
$original_location,
|
||||
$unused_param_message,
|
||||
$unused_param_code_location,
|
||||
),
|
||||
$this->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new UnusedParam(
|
||||
'Param ' . $var_name . ' is never referenced in this method',
|
||||
$original_location,
|
||||
),
|
||||
$this->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$fq_class_name = (string)$context->self;
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
|
||||
|
||||
$method_name_lc = strtolower($storage->cased_name);
|
||||
|
||||
if ($storage->abstract) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($class_storage->overridden_method_ids[$method_name_lc])) {
|
||||
$parent_method_id = end($class_storage->overridden_method_ids[$method_name_lc]);
|
||||
|
||||
if ($parent_method_id) {
|
||||
$parent_method_storage = $codebase->methods->getStorage($parent_method_id);
|
||||
|
||||
// if the parent method has a param at that position and isn't abstract
|
||||
if (!$parent_method_storage->abstract
|
||||
&& isset($parent_method_storage->params[$position])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unused_params[$position] = $original_location;
|
||||
IssueBuffer::maybeAdd(
|
||||
new UnusedParam(
|
||||
$unused_param_message,
|
||||
$unused_param_code_location,
|
||||
),
|
||||
$this->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2076,4 +2041,120 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
$overridden_method_ids,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,CodeLocation>
|
||||
*/
|
||||
private function detectUnusedParameters(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
FunctionLikeStorage $storage,
|
||||
Context $context
|
||||
): array {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$unused_params = [];
|
||||
|
||||
foreach ($statements_analyzer->getUnusedVarLocations() as [$var_name, $original_location]) {
|
||||
if (!array_key_exists(substr($var_name, 1), $storage->param_lookup)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isIgnoredForUnusedParam($var_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$position = array_search(substr($var_name, 1), array_keys($storage->param_lookup), true);
|
||||
|
||||
if ($position === false) {
|
||||
throw new UnexpectedValueException('$position should not be false here');
|
||||
}
|
||||
|
||||
if ($storage->params[$position]->promoted_property) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$did_match_param = false;
|
||||
|
||||
foreach ($this->function->params as $param) {
|
||||
if ($param->var->getAttribute('endFilePos') === $original_location->raw_file_end) {
|
||||
$did_match_param = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$did_match_param) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$assignment_node = DataFlowNode::getForAssignment($var_name, $original_location);
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
|
||||
&& $statements_analyzer->data_flow_graph->isVariableUsed($assignment_node)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$storage instanceof MethodStorage
|
||||
|| !$storage->cased_name
|
||||
|| $storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
) {
|
||||
$unused_params[$position] = $original_location;
|
||||
continue;
|
||||
}
|
||||
|
||||
$fq_class_name = (string)$context->self;
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
|
||||
|
||||
$method_name_lc = strtolower($storage->cased_name);
|
||||
|
||||
if ($storage->abstract) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($class_storage->overridden_method_ids[$method_name_lc])) {
|
||||
$parent_method_id = end($class_storage->overridden_method_ids[$method_name_lc]);
|
||||
|
||||
if ($parent_method_id) {
|
||||
$parent_method_storage = $codebase->methods->getStorage($parent_method_id);
|
||||
|
||||
// if the parent method has a param at that position and isn't abstract
|
||||
if (!$parent_method_storage->abstract
|
||||
&& isset($parent_method_storage->params[$position])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unused_params[$position] = $original_location;
|
||||
}
|
||||
|
||||
return $unused_params;
|
||||
}
|
||||
|
||||
private function detectPreviousUnusedArgumentPosition(FunctionLikeStorage $function, int $position): int
|
||||
{
|
||||
$params = $function->params;
|
||||
krsort($params, SORT_NUMERIC);
|
||||
|
||||
foreach ($params as $index => $param) {
|
||||
if ($index > $position) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isIgnoredForUnusedParam($param->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function isIgnoredForUnusedParam(string $var_name): bool
|
||||
{
|
||||
return strpos($var_name, '$_') === 0 || (strpos($var_name, '$unused') === 0 && $var_name !== '$unused');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use UnexpectedValueException;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_string;
|
||||
use function preg_replace;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
@@ -244,7 +245,7 @@ class NamespaceAnalyzer extends SourceAnalyzer
|
||||
while (($pos = strpos($identifier, "\\")) !== false) {
|
||||
if ($pos > 0) {
|
||||
$part = substr($identifier, 0, $pos);
|
||||
assert($part !== "");
|
||||
assert(is_string($part) && $part !== "");
|
||||
$parts[] = $part;
|
||||
}
|
||||
$parts[] = "\\";
|
||||
@@ -253,13 +254,13 @@ class NamespaceAnalyzer extends SourceAnalyzer
|
||||
if (($pos = strpos($identifier, "::")) !== false) {
|
||||
if ($pos > 0) {
|
||||
$part = substr($identifier, 0, $pos);
|
||||
assert($part !== "");
|
||||
assert(is_string($part) && $part !== "");
|
||||
$parts[] = $part;
|
||||
}
|
||||
$parts[] = "::";
|
||||
$identifier = substr($identifier, $pos + 2);
|
||||
}
|
||||
if ($identifier !== "") {
|
||||
if ($identifier !== "" && $identifier !== false) {
|
||||
$parts[] = $identifier;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use Amp\Loop;
|
||||
use Fidry\CpuCoreCounter\CpuCoreCounter;
|
||||
use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound;
|
||||
use InvalidArgumentException;
|
||||
@@ -15,8 +14,6 @@ use Psalm\FileManipulation;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer;
|
||||
use Psalm\Internal\LanguageServer\ProtocolStreamReader;
|
||||
use Psalm\Internal\LanguageServer\ProtocolStreamWriter;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Provider\ClassLikeStorageProvider;
|
||||
use Psalm\Internal\Provider\FileProvider;
|
||||
@@ -65,7 +62,6 @@ use function array_map;
|
||||
use function array_merge;
|
||||
use function array_shift;
|
||||
use function clearstatcache;
|
||||
use function cli_set_process_title;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function dirname;
|
||||
@@ -82,14 +78,9 @@ use function is_file;
|
||||
use function microtime;
|
||||
use function mkdir;
|
||||
use function number_format;
|
||||
use function pcntl_fork;
|
||||
use function preg_match;
|
||||
use function rename;
|
||||
use function sprintf;
|
||||
use function stream_set_blocking;
|
||||
use function stream_socket_accept;
|
||||
use function stream_socket_client;
|
||||
use function stream_socket_server;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
@@ -102,8 +93,6 @@ use const PHP_OS;
|
||||
use const PHP_VERSION;
|
||||
use const PSALM_VERSION;
|
||||
use const STDERR;
|
||||
use const STDIN;
|
||||
use const STDOUT;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -211,16 +200,6 @@ class ProjectAnalyzer
|
||||
UnnecessaryVarAnnotation::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* When this is true, the language server will send the diagnostic code with a help link.
|
||||
*/
|
||||
public bool $language_server_use_extended_diagnostic_codes = false;
|
||||
|
||||
/**
|
||||
* If this is true then the language server will send log messages to the client with additional information.
|
||||
*/
|
||||
public bool $language_server_verbose = false;
|
||||
|
||||
/**
|
||||
* @param array<ReportOptions> $generated_report_options
|
||||
*/
|
||||
@@ -230,12 +209,21 @@ class ProjectAnalyzer
|
||||
?ReportOptions $stdout_report_options = null,
|
||||
array $generated_report_options = [],
|
||||
int $threads = 1,
|
||||
?Progress $progress = null
|
||||
?Progress $progress = null,
|
||||
?Codebase $codebase = null
|
||||
) {
|
||||
if ($progress === null) {
|
||||
$progress = new VoidProgress();
|
||||
}
|
||||
|
||||
if ($codebase === null) {
|
||||
$codebase = new Codebase(
|
||||
$config,
|
||||
$providers,
|
||||
$progress,
|
||||
);
|
||||
}
|
||||
|
||||
$this->parser_cache_provider = $providers->parser_cache_provider;
|
||||
$this->project_cache_provider = $providers->project_cache_provider;
|
||||
$this->file_provider = $providers->file_provider;
|
||||
@@ -248,11 +236,7 @@ class ProjectAnalyzer
|
||||
|
||||
$this->clearCacheDirectoryIfConfigOrComposerLockfileChanged();
|
||||
|
||||
$this->codebase = new Codebase(
|
||||
$config,
|
||||
$providers,
|
||||
$progress,
|
||||
);
|
||||
$this->codebase = $codebase;
|
||||
|
||||
$this->stdout_report_options = $stdout_report_options;
|
||||
$this->generated_report_options = $generated_report_options;
|
||||
@@ -394,10 +378,12 @@ class ProjectAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void
|
||||
public function serverMode(LanguageServer $server): void
|
||||
{
|
||||
$server->logInfo("Initializing: Visiting Autoload Files...");
|
||||
$this->visitAutoloadFiles();
|
||||
$this->codebase->diff_methods = true;
|
||||
$server->logInfo("Initializing: Loading Reference Cache...");
|
||||
$this->file_reference_provider->loadReferenceCache();
|
||||
$this->codebase->enterServerMode();
|
||||
|
||||
@@ -418,103 +404,12 @@ class ProjectAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$server->logInfo("Initializing: Initialize Plugins...");
|
||||
$this->config->initializePlugins($this);
|
||||
|
||||
foreach ($this->config->getProjectDirectories() as $dir_name) {
|
||||
$this->checkDirWithConfig($dir_name, $this->config);
|
||||
}
|
||||
|
||||
@cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server');
|
||||
|
||||
if (!$socket_server_mode && $address) {
|
||||
// Connect to a TCP server
|
||||
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
|
||||
if ($socket === false) {
|
||||
fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr");
|
||||
exit(1);
|
||||
}
|
||||
stream_set_blocking($socket, false);
|
||||
new LanguageServer(
|
||||
new ProtocolStreamReader($socket),
|
||||
new ProtocolStreamWriter($socket),
|
||||
$this,
|
||||
);
|
||||
Loop::run();
|
||||
} elseif ($socket_server_mode && $address) {
|
||||
// Run a TCP Server
|
||||
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
|
||||
if ($tcpServer === false) {
|
||||
fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr");
|
||||
exit(1);
|
||||
}
|
||||
fwrite(STDOUT, "Server listening on $address\n");
|
||||
|
||||
$fork_available = true;
|
||||
if (!extension_loaded('pcntl')) {
|
||||
fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n");
|
||||
$fork_available = false;
|
||||
}
|
||||
|
||||
$disabled_functions = array_map('trim', explode(',', ini_get('disable_functions')));
|
||||
if (in_array('pcntl_fork', $disabled_functions)) {
|
||||
fwrite(
|
||||
STDERR,
|
||||
"pcntl_fork() is disabled by php configuration (disable_functions directive)."
|
||||
. " Only a single connection will be accepted\n",
|
||||
);
|
||||
$fork_available = false;
|
||||
}
|
||||
|
||||
while ($socket = stream_socket_accept($tcpServer, -1)) {
|
||||
fwrite(STDOUT, "Connection accepted\n");
|
||||
stream_set_blocking($socket, false);
|
||||
if ($fork_available) {
|
||||
// If PCNTL is available, fork a child process for the connection
|
||||
// An exit notification will only terminate the child process
|
||||
$pid = pcntl_fork();
|
||||
if ($pid === -1) {
|
||||
fwrite(STDERR, "Could not fork\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($pid === 0) {
|
||||
// Child process
|
||||
$reader = new ProtocolStreamReader($socket);
|
||||
$reader->on(
|
||||
'close',
|
||||
static function (): void {
|
||||
fwrite(STDOUT, "Connection closed\n");
|
||||
},
|
||||
);
|
||||
new LanguageServer(
|
||||
$reader,
|
||||
new ProtocolStreamWriter($socket),
|
||||
$this,
|
||||
);
|
||||
// Just for safety
|
||||
exit(0);
|
||||
}
|
||||
} else {
|
||||
// If PCNTL is not available, we only accept one connection.
|
||||
// An exit notification will terminate the server
|
||||
new LanguageServer(
|
||||
new ProtocolStreamReader($socket),
|
||||
new ProtocolStreamWriter($socket),
|
||||
$this,
|
||||
);
|
||||
Loop::run();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use STDIO
|
||||
stream_set_blocking(STDIN, false);
|
||||
new LanguageServer(
|
||||
new ProtocolStreamReader(STDIN),
|
||||
new ProtocolStreamWriter(STDOUT),
|
||||
$this,
|
||||
);
|
||||
Loop::run();
|
||||
}
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
@@ -1244,6 +1139,11 @@ class ProjectAnalyzer
|
||||
return $this->file_provider->fileExists($file_path);
|
||||
}
|
||||
|
||||
public function isDirectory(string $file_path): bool
|
||||
{
|
||||
return $this->file_provider->isDirectory($file_path);
|
||||
}
|
||||
|
||||
public function alterCodeAfterCompletion(
|
||||
bool $dry_run = false,
|
||||
bool $safe_types = false
|
||||
|
||||
@@ -42,6 +42,7 @@ use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\Scalar;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TCallableObject;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TIterable;
|
||||
@@ -190,7 +191,7 @@ class ForeachAnalyzer
|
||||
&& $type_location
|
||||
&& isset($context->vars_in_scope[$var_comment->var_id])
|
||||
&& $context->vars_in_scope[$var_comment->var_id]->getId() === $comment_type->getId()
|
||||
&& !$comment_type->isMixed()
|
||||
&& !$comment_type->isMixed(true)
|
||||
) {
|
||||
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
|
||||
|
||||
@@ -266,10 +267,6 @@ class ForeachAnalyzer
|
||||
|
||||
$foreach_context = clone $context;
|
||||
|
||||
foreach ($foreach_context->vars_in_scope as $context_var_id => $context_type) {
|
||||
$foreach_context->vars_in_scope[$context_var_id] = $context_type;
|
||||
}
|
||||
|
||||
if ($var_id && $foreach_context->hasVariable($var_id)) {
|
||||
// refine the type of the array variable we iterate over
|
||||
// if we entered loop body, the array cannot be empty
|
||||
@@ -750,6 +747,7 @@ class ForeachAnalyzer
|
||||
foreach ($iterator_atomic_types as $iterator_atomic_type) {
|
||||
if ($iterator_atomic_type instanceof TTemplateParam
|
||||
|| $iterator_atomic_type instanceof TObjectWithProperties
|
||||
|| $iterator_atomic_type instanceof TCallableObject
|
||||
) {
|
||||
throw new UnexpectedValueException('Shouldn’t get a generic param here');
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ class ElseAnalyzer
|
||||
$original_context,
|
||||
$new_assigned_var_ids,
|
||||
$new_possibly_assigned_var_ids,
|
||||
[],
|
||||
$if_scope->if_cond_changed_var_ids,
|
||||
true,
|
||||
);
|
||||
|
||||
|
||||
@@ -108,11 +108,6 @@ class LoopAnalyzer
|
||||
|
||||
if ($assignment_depth === 0 || $does_always_break) {
|
||||
$continue_context = clone $loop_context;
|
||||
|
||||
foreach ($continue_context->vars_in_scope as $context_var_id => $context_type) {
|
||||
$continue_context->vars_in_scope[$context_var_id] = $context_type;
|
||||
}
|
||||
|
||||
$continue_context->loop_scope = $loop_scope;
|
||||
|
||||
foreach ($pre_conditions as $condition_offset => $pre_condition) {
|
||||
|
||||
@@ -333,19 +333,32 @@ class ArrayAnalyzer
|
||||
} elseif ($key_type->isSingleIntLiteral()) {
|
||||
$item_key_value = $key_type->getSingleIntLiteral()->value;
|
||||
|
||||
if ($item_key_value >= $array_creation_info->int_offset) {
|
||||
if ($item_key_value === $array_creation_info->int_offset) {
|
||||
if ($item_key_value <= PHP_INT_MAX
|
||||
&& $item_key_value > $array_creation_info->int_offset
|
||||
) {
|
||||
if ($item_key_value - 1 === $array_creation_info->int_offset) {
|
||||
$item_is_list_item = true;
|
||||
}
|
||||
$array_creation_info->int_offset = $item_key_value + 1;
|
||||
$array_creation_info->int_offset = $item_key_value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$key_type = Type::getArrayKey();
|
||||
}
|
||||
} else {
|
||||
if ($array_creation_info->int_offset === PHP_INT_MAX) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidArrayOffset(
|
||||
'Cannot add an item with an offset beyond PHP_INT_MAX',
|
||||
new CodeLocation($statements_analyzer->getSource(), $item),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$item_is_list_item = true;
|
||||
$item_key_value = $array_creation_info->int_offset++;
|
||||
$item_key_value = ++$array_creation_info->int_offset;
|
||||
|
||||
$key_atomic_type = new TLiteralInt($item_key_value);
|
||||
$array_creation_info->item_key_atomic_types[] = $key_atomic_type;
|
||||
$key_type = new Union([$key_atomic_type]);
|
||||
@@ -538,7 +551,17 @@ class ArrayAnalyzer
|
||||
$array_creation_info->item_key_atomic_types[] = Type::getAtomicStringFromLiteral($new_offset);
|
||||
$array_creation_info->all_list = false;
|
||||
} else {
|
||||
$new_offset = $array_creation_info->int_offset++;
|
||||
if ($array_creation_info->int_offset === PHP_INT_MAX) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidArrayOffset(
|
||||
'Cannot add an item with an offset beyond PHP_INT_MAX',
|
||||
new CodeLocation($statements_analyzer->getSource(), $item->value),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
$new_offset = ++$array_creation_info->int_offset;
|
||||
$array_creation_info->item_key_atomic_types[] = new TLiteralInt($new_offset);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,12 @@ class ArrayCreationInfo
|
||||
*/
|
||||
public array $array_keys = [];
|
||||
|
||||
public int $int_offset = 0;
|
||||
/**
|
||||
* Holds the integer offset of the *last* element added
|
||||
*
|
||||
* -1 may mean no elements have been added yet, but can also mean there's an element with offset -1
|
||||
*/
|
||||
public int $int_offset = -1;
|
||||
|
||||
public bool $all_list = true;
|
||||
|
||||
|
||||
@@ -1087,7 +1087,7 @@ class InstancePropertyAssignmentAnalyzer
|
||||
* If we have an explicit list of all allowed magic properties on the class, and we're
|
||||
* not in that list, fall through
|
||||
*/
|
||||
if (!$var_id || !$class_storage->sealed_properties) {
|
||||
if (!$var_id || !$class_storage->hasSealedProperties($codebase->config)) {
|
||||
if (!$context->collect_initializations && !$context->collect_mutations) {
|
||||
self::taintProperty(
|
||||
$statements_analyzer,
|
||||
|
||||
@@ -56,6 +56,7 @@ use Psalm\Issue\PossiblyUndefinedIntArrayOffset;
|
||||
use Psalm\Issue\ReferenceConstraintViolation;
|
||||
use Psalm\Issue\ReferenceReusedFromConfusingScope;
|
||||
use Psalm\Issue\UnnecessaryVarAnnotation;
|
||||
use Psalm\Issue\UnsupportedPropertyReferenceUsage;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualBitwiseAnd;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualBitwiseOr;
|
||||
@@ -270,7 +271,7 @@ class AssignmentAnalyzer
|
||||
&& $extended_var_id
|
||||
&& (!$not_ignored_docblock_var_ids || isset($not_ignored_docblock_var_ids[$extended_var_id]))
|
||||
&& $temp_assign_value_type->getId() === $comment_type->getId()
|
||||
&& !$comment_type->isMixed()
|
||||
&& !$comment_type->isMixed(true)
|
||||
) {
|
||||
if ($codebase->alter_code
|
||||
&& isset($statements_analyzer->getProjectAnalyzer()->getIssuesToFix()['UnnecessaryVarAnnotation'])
|
||||
@@ -980,10 +981,18 @@ class AssignmentAnalyzer
|
||||
$context->references_to_external_scope[$lhs_var_id] = true;
|
||||
}
|
||||
if (strpos($rhs_var_id, '->') !== false) {
|
||||
IssueBuffer::maybeAdd(new UnsupportedPropertyReferenceUsage(
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
));
|
||||
// Reference to object property, we always consider object properties to be an external scope for references
|
||||
// TODO handle differently so it's detected as unused if the object is unused?
|
||||
$context->references_to_external_scope[$lhs_var_id] = true;
|
||||
}
|
||||
if (strpos($rhs_var_id, '::') !== false) {
|
||||
IssueBuffer::maybeAdd(new UnsupportedPropertyReferenceUsage(
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
));
|
||||
}
|
||||
|
||||
$lhs_location = new CodeLocation($statements_analyzer->getSource(), $stmt->var);
|
||||
if (!$stmt->var instanceof ArrayDimFetch && !$stmt->var instanceof PropertyFetch) {
|
||||
|
||||
@@ -71,7 +71,9 @@ use function reset;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function substr_count;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PREG_SPLIT_NO_EMPTY;
|
||||
|
||||
/**
|
||||
@@ -174,7 +176,9 @@ class ArgumentAnalyzer
|
||||
$prev_ord = $ord;
|
||||
}
|
||||
|
||||
if (count($values) < 12 || ($gt_count / count($values)) < 0.8) {
|
||||
if (substr_count($arg_value_type->getSingleStringLiteral()->value, DIRECTORY_SEPARATOR) <= 2
|
||||
&& (count($values) < 12 || ($gt_count / count($values)) < 0.8)
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidLiteralArgument(
|
||||
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
|
||||
|
||||
@@ -20,7 +20,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\DataFlow\TaintSink;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Stubs\Generator\StubsGenerator;
|
||||
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
@@ -197,18 +196,16 @@ class ArgumentsAnalyzer
|
||||
}
|
||||
|
||||
$high_order_template_result = null;
|
||||
$high_order_callable_info = $param
|
||||
? HighOrderFunctionArgHandler::getCallableArgInfo($context, $arg->value, $statements_analyzer, $param)
|
||||
: null;
|
||||
|
||||
if (($arg->value instanceof PhpParser\Node\Expr\FuncCall
|
||||
|| $arg->value instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $arg->value instanceof PhpParser\Node\Expr\StaticCall)
|
||||
&& $param
|
||||
&& $function_storage = self::getHighOrderFuncStorage($context, $statements_analyzer, $arg->value)
|
||||
) {
|
||||
$high_order_template_result = self::handleHighOrderFuncCallArg(
|
||||
if ($param && $high_order_callable_info) {
|
||||
$high_order_template_result = HighOrderFunctionArgHandler::remapLowerBounds(
|
||||
$statements_analyzer,
|
||||
$template_result ?? new TemplateResult([], []),
|
||||
$function_storage,
|
||||
$param,
|
||||
$high_order_callable_info,
|
||||
$param->type ?? Type::getMixed(),
|
||||
);
|
||||
} elseif (($arg->value instanceof PhpParser\Node\Expr\Closure
|
||||
|| $arg->value instanceof PhpParser\Node\Expr\ArrowFunction)
|
||||
@@ -228,7 +225,6 @@ class ArgumentsAnalyzer
|
||||
}
|
||||
|
||||
$was_inside_call = $context->inside_call;
|
||||
|
||||
$context->inside_call = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
@@ -247,6 +243,16 @@ class ArgumentsAnalyzer
|
||||
|
||||
$context->inside_call = $was_inside_call;
|
||||
|
||||
if ($high_order_callable_info && $high_order_template_result) {
|
||||
HighOrderFunctionArgHandler::enhanceCallableArgType(
|
||||
$context,
|
||||
$arg->value,
|
||||
$statements_analyzer,
|
||||
$high_order_callable_info,
|
||||
$high_order_template_result,
|
||||
);
|
||||
}
|
||||
|
||||
if (($argument_offset === 0 && $method_id === 'array_filter' && count($args) === 2)
|
||||
|| ($argument_offset > 0 && $method_id === 'array_map' && count($args) >= 2)
|
||||
) {
|
||||
@@ -345,184 +351,6 @@ class ArgumentsAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private static function getHighOrderFuncStorage(
|
||||
Context $context,
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\CallLike $function_like_call
|
||||
): ?FunctionLikeStorage {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
try {
|
||||
if ($function_like_call instanceof PhpParser\Node\Expr\FuncCall &&
|
||||
!$function_like_call->isFirstClassCallable()
|
||||
) {
|
||||
$function_id = strtolower((string) $function_like_call->name->getAttribute('resolvedName'));
|
||||
|
||||
if (empty($function_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($codebase->functions->dynamic_storage_provider->has($function_id)) {
|
||||
return $codebase->functions->dynamic_storage_provider->getFunctionStorage(
|
||||
$function_like_call,
|
||||
$statements_analyzer,
|
||||
$function_id,
|
||||
$context,
|
||||
new CodeLocation($statements_analyzer, $function_like_call),
|
||||
);
|
||||
}
|
||||
|
||||
return $codebase->functions->getStorage($statements_analyzer, $function_id);
|
||||
}
|
||||
|
||||
if ($function_like_call instanceof PhpParser\Node\Expr\MethodCall &&
|
||||
$function_like_call->var instanceof PhpParser\Node\Expr\Variable &&
|
||||
$function_like_call->name instanceof PhpParser\Node\Identifier &&
|
||||
is_string($function_like_call->var->name) &&
|
||||
isset($context->vars_in_scope['$' . $function_like_call->var->name])
|
||||
) {
|
||||
$lhs_type = $context->vars_in_scope['$' . $function_like_call->var->name]->getSingleAtomic();
|
||||
|
||||
if (!$lhs_type instanceof Type\Atomic\TNamedObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$method_id = new MethodIdentifier(
|
||||
$lhs_type->value,
|
||||
strtolower((string)$function_like_call->name),
|
||||
);
|
||||
|
||||
return $codebase->methods->getStorage($method_id);
|
||||
}
|
||||
|
||||
if ($function_like_call instanceof PhpParser\Node\Expr\StaticCall &&
|
||||
$function_like_call->name instanceof PhpParser\Node\Identifier
|
||||
) {
|
||||
$method_id = new MethodIdentifier(
|
||||
(string)$function_like_call->class->getAttribute('resolvedName'),
|
||||
strtolower($function_like_call->name->name),
|
||||
);
|
||||
|
||||
return $codebase->methods->getStorage($method_id);
|
||||
}
|
||||
} catch (UnexpectedValueException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles TemplateResult for high-order functions ($func_call)
|
||||
* by previous template args ($inferred_template_result).
|
||||
*
|
||||
* It's need for proper template replacement:
|
||||
*
|
||||
* ```
|
||||
* * template T
|
||||
* * return Closure(T): T
|
||||
* function id(): Closure { ... }
|
||||
*
|
||||
* * template A
|
||||
* * template B
|
||||
* *
|
||||
* * param list<A> $_items
|
||||
* * param callable(A): B $_ab
|
||||
* * return list<B>
|
||||
* function map(array $items, callable $ab): array { ... }
|
||||
*
|
||||
* // list<int>
|
||||
* $numbers = [1, 2, 3];
|
||||
*
|
||||
* $result = map($numbers, id());
|
||||
* // $result is list<int> because template T of id() was inferred by previous arg.
|
||||
* ```
|
||||
*/
|
||||
private static function handleHighOrderFuncCallArg(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
TemplateResult $inferred_template_result,
|
||||
FunctionLikeStorage $storage,
|
||||
FunctionLikeParameter $actual_func_param
|
||||
): ?TemplateResult {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$input_hof_atomic = $storage->return_type && $storage->return_type->isSingle()
|
||||
? $storage->return_type->getSingleAtomic()
|
||||
: null;
|
||||
|
||||
// Try upcast invokable to callable type.
|
||||
if ($input_hof_atomic instanceof Type\Atomic\TNamedObject &&
|
||||
$input_hof_atomic->value !== 'Closure' &&
|
||||
$codebase->classExists($input_hof_atomic->value)
|
||||
) {
|
||||
$callable_from_invokable = CallableTypeComparator::getCallableFromAtomic(
|
||||
$codebase,
|
||||
$input_hof_atomic,
|
||||
);
|
||||
|
||||
if ($callable_from_invokable) {
|
||||
$invoke_id = new MethodIdentifier($input_hof_atomic->value, '__invoke');
|
||||
$declaring_invoke_id = $codebase->methods->getDeclaringMethodId($invoke_id);
|
||||
|
||||
$storage = $codebase->methods->getStorage($declaring_invoke_id ?? $invoke_id);
|
||||
$input_hof_atomic = $callable_from_invokable;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$input_hof_atomic instanceof TClosure && !$input_hof_atomic instanceof TCallable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$container_hof_atomic = $actual_func_param->type && $actual_func_param->type->isSingle()
|
||||
? $actual_func_param->type->getSingleAtomic()
|
||||
: null;
|
||||
|
||||
if (!$container_hof_atomic instanceof TClosure && !$container_hof_atomic instanceof TCallable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$replaced_container_hof_atomic = new Union([$container_hof_atomic]);
|
||||
|
||||
// Replaces all input args in container function.
|
||||
//
|
||||
// For example:
|
||||
// The map function expects callable(A):B as second param
|
||||
// We know that previous arg type is list<int> where the int is the A template.
|
||||
// Then we can replace callable(A): B to callable(int):B using $inferred_template_result.
|
||||
$replaced_container_hof_atomic = TemplateInferredTypeReplacer::replace(
|
||||
$replaced_container_hof_atomic,
|
||||
$inferred_template_result,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
/** @var TClosure|TCallable $container_hof_atomic */
|
||||
$container_hof_atomic = $replaced_container_hof_atomic->getSingleAtomic();
|
||||
$high_order_template_result = new TemplateResult($storage->template_types ?: [], []);
|
||||
|
||||
// We can replace each templated param for the input function.
|
||||
// Example:
|
||||
// map($numbers, id());
|
||||
// We know that map expects callable(int):B because the $numbers is list<int>.
|
||||
// We know that id() returns callable(T):T.
|
||||
// Then we can replace templated params sequentially using the expected callable(int):B.
|
||||
foreach ($input_hof_atomic->params ?? [] as $offset => $actual_func_param) {
|
||||
if ($actual_func_param->type &&
|
||||
$actual_func_param->type->getTemplateTypes() &&
|
||||
isset($container_hof_atomic->params[$offset])
|
||||
) {
|
||||
TemplateStandinTypeReplacer::fillTemplateResult(
|
||||
$actual_func_param->type,
|
||||
$high_order_template_result,
|
||||
$codebase,
|
||||
null,
|
||||
$container_hof_atomic->params[$offset]->type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $high_order_template_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
*/
|
||||
@@ -930,7 +758,7 @@ class ArgumentsAnalyzer
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidNamedArgument(
|
||||
'Parameter $' . $key_type->value . ' does not exist on function '
|
||||
. ($cased_method_id ?: $method_id),
|
||||
. ($cased_method_id ?: $method_id),
|
||||
new CodeLocation($statements_analyzer, $arg),
|
||||
(string)$method_id,
|
||||
),
|
||||
@@ -970,7 +798,7 @@ class ArgumentsAnalyzer
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidNamedArgument(
|
||||
'Parameter $' . $arg->name->name . ' does not exist on function '
|
||||
. ($cased_method_id ?: $method_id),
|
||||
. ($cased_method_id ?: $method_id),
|
||||
new CodeLocation($statements_analyzer, $arg->name),
|
||||
(string) $method_id,
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Type\ArrayType;
|
||||
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
@@ -41,12 +42,14 @@ use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function array_shift;
|
||||
use function array_unshift;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function is_numeric;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
@@ -346,6 +349,40 @@ class ArrayFunctionArgumentsAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
$array_type = null;
|
||||
$array_size = null;
|
||||
|
||||
if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
|
||||
&& $array_arg_type->hasArray()
|
||||
) {
|
||||
/**
|
||||
* @var TArray|TKeyedArray
|
||||
*/
|
||||
$array_type = $array_arg_type->getArray();
|
||||
if ($generic_array_type = ArrayType::infer($array_type)) {
|
||||
$array_size = $generic_array_type->count;
|
||||
}
|
||||
|
||||
if ($array_type instanceof TKeyedArray) {
|
||||
if ($array_type->is_list && isset($args[3])) {
|
||||
$array_type = Type::getNonEmptyListAtomic($array_type->getGenericValueType());
|
||||
} else {
|
||||
$array_type = $array_type->getGenericArrayType();
|
||||
}
|
||||
}
|
||||
|
||||
if ($array_type instanceof TArray
|
||||
&& $array_type->type_params[0]->hasInt()
|
||||
&& !$array_type->type_params[0]->hasString()
|
||||
) {
|
||||
if ($array_type instanceof TNonEmptyArray && isset($args[3])) {
|
||||
$array_type = Type::getNonEmptyListAtomic($array_type->type_params[1]);
|
||||
} else {
|
||||
$array_type = Type::getListAtomic($array_type->type_params[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$offset_arg = $args[1]->value;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
@@ -356,7 +393,47 @@ class ArrayFunctionArgumentsAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
$offset_arg_is_zero = false;
|
||||
|
||||
if (($offset_arg_type = $statements_analyzer->node_data->getType($offset_arg))
|
||||
&& $offset_arg_type->hasLiteralValue() && $offset_arg_type->isSingleLiteral()
|
||||
) {
|
||||
$offset_literal_value = $offset_arg_type->getSingleLiteral()->value;
|
||||
$offset_arg_is_zero = is_numeric($offset_literal_value) && ((int) $offset_literal_value)===0;
|
||||
}
|
||||
|
||||
if (!isset($args[2])) {
|
||||
if ($offset_arg_is_zero) {
|
||||
$array_type = Type::getEmptyArray();
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$array_type,
|
||||
$array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
} elseif ($array_type) {
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
new Union([$array_type]),
|
||||
new Union([$array_type]),
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
$default_array_type = Type::getArray();
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$default_array_type,
|
||||
$default_array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -370,7 +447,69 @@ class ArrayFunctionArgumentsAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
$cover_whole_arr = false;
|
||||
if ($offset_arg_is_zero && is_numeric($array_size)) {
|
||||
if (($length_arg_type = $statements_analyzer->node_data->getType($length_arg))
|
||||
&& $length_arg_type->hasLiteralValue()
|
||||
) {
|
||||
$length_min = null;
|
||||
if ($length_arg_type->isSingleLiteral()) {
|
||||
$length_literal = $length_arg_type->getSingleLiteral();
|
||||
if ($length_literal->isNumericType()) {
|
||||
$length_min = (int) $length_literal->value;
|
||||
}
|
||||
} else {
|
||||
$literals = array_merge(
|
||||
$length_arg_type->getLiteralStrings(),
|
||||
$length_arg_type->getLiteralInts(),
|
||||
$length_arg_type->getLiteralFloats(),
|
||||
);
|
||||
foreach ($literals as $literal) {
|
||||
if ($literal->isNumericType()
|
||||
&& ($literal_val = (int) $literal->value)
|
||||
&& ((isset($length_min) && $length_min> $literal_val) || !isset($length_min))) {
|
||||
$length_min = $literal_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
$cover_whole_arr = isset($length_min) && $length_min>= $array_size;
|
||||
} elseif ($length_arg_type&& $length_arg_type->isNull()) {
|
||||
$cover_whole_arr = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($args[3])) {
|
||||
if ($cover_whole_arr) {
|
||||
$array_type = Type::getEmptyArray();
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$array_type,
|
||||
$array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
} elseif ($array_type) {
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
new Union([$array_type]),
|
||||
new Union([$array_type]),
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
$default_array_type = Type::getArray();
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$default_array_type,
|
||||
$default_array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -400,40 +539,31 @@ class ArrayFunctionArgumentsAnalyzer
|
||||
$statements_analyzer->node_data->setType($replacement_arg, $replacement_arg_type);
|
||||
}
|
||||
|
||||
if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
|
||||
&& $array_arg_type->hasArray()
|
||||
if ($array_type
|
||||
&& $replacement_arg_type
|
||||
&& $replacement_arg_type->hasArray()
|
||||
) {
|
||||
/**
|
||||
* @var TArray|TKeyedArray
|
||||
*/
|
||||
$array_type = $array_arg_type->getArray();
|
||||
|
||||
if ($array_type instanceof TKeyedArray) {
|
||||
if ($array_type->is_list) {
|
||||
$array_type = Type::getNonEmptyListAtomic($array_type->getGenericValueType());
|
||||
} else {
|
||||
$array_type = $array_type->getGenericArrayType();
|
||||
}
|
||||
}
|
||||
|
||||
if ($array_type instanceof TArray
|
||||
&& $array_type->type_params[0]->hasInt()
|
||||
&& !$array_type->type_params[0]->hasString()
|
||||
) {
|
||||
if ($array_type instanceof TNonEmptyArray) {
|
||||
$array_type = Type::getNonEmptyListAtomic($array_type->type_params[1]);
|
||||
} else {
|
||||
$array_type = Type::getListAtomic($array_type->type_params[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var TArray|TKeyedArray
|
||||
*/
|
||||
$replacement_array_type = $replacement_arg_type->getArray();
|
||||
|
||||
if (($replacement_array_type_generic = ArrayType::infer($replacement_array_type))
|
||||
&& $replacement_array_type_generic->count === 0
|
||||
&& $cover_whole_arr) {
|
||||
$empty_array_type = Type::getEmptyArray();
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$empty_array_type,
|
||||
$empty_array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($replacement_array_type instanceof TKeyedArray) {
|
||||
$was_list = $replacement_array_type->is_list;
|
||||
|
||||
@@ -462,16 +592,26 @@ class ArrayFunctionArgumentsAnalyzer
|
||||
return null;
|
||||
}
|
||||
|
||||
$array_type = Type::getArray();
|
||||
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$array_type,
|
||||
$array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
if ($array_type) {
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
new Union([$array_type]),
|
||||
new Union([$array_type]),
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
$default_array_type = Type::getArray();
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$default_array_type,
|
||||
$default_array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -723,8 +723,17 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} elseif ($var_type_part instanceof TCallableObject
|
||||
|| $var_type_part instanceof TCallableString
|
||||
} elseif ($var_type_part instanceof TCallableObject) {
|
||||
$has_valid_function_call_type = true;
|
||||
self::analyzeInvokeCall(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$real_stmt,
|
||||
$function_name,
|
||||
$context,
|
||||
$var_type_part,
|
||||
);
|
||||
} elseif ($var_type_part instanceof TCallableString
|
||||
|| ($var_type_part instanceof TNamedObject && $var_type_part->value === 'Closure')
|
||||
|| ($var_type_part instanceof TObjectWithProperties && isset($var_type_part->methods['__invoke']))
|
||||
) {
|
||||
|
||||
@@ -482,15 +482,25 @@ class FunctionCallReturnTypeFetcher
|
||||
|
||||
return $call_map_return_type;
|
||||
case 'mb_strtolower':
|
||||
$string_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value);
|
||||
if ($string_arg_type !== null && $string_arg_type->isNonEmptyString()) {
|
||||
$returnType = Type::getNonEmptyLowercaseString();
|
||||
} else {
|
||||
$returnType = Type::getLowercaseString();
|
||||
}
|
||||
if (count($call_args) < 2) {
|
||||
return Type::getLowercaseString();
|
||||
return $returnType;
|
||||
} else {
|
||||
$second_arg_type = $statements_analyzer->node_data->getType($call_args[1]->value);
|
||||
if ($second_arg_type && $second_arg_type->isNull()) {
|
||||
return Type::getLowercaseString();
|
||||
return $returnType;
|
||||
}
|
||||
}
|
||||
return Type::getString();
|
||||
if ($string_arg_type !== null && $string_arg_type->isNonEmptyString()) {
|
||||
return Type::getNonEmptyString();
|
||||
} else {
|
||||
return Type::getString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Storage\FunctionLikeParameter;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TCallable;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function is_string;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class HighOrderFunctionArgHandler
|
||||
{
|
||||
/**
|
||||
* Compiles TemplateResult for high-order function
|
||||
* by previous template args ($inferred_template_result).
|
||||
*
|
||||
* It's need for proper template replacement:
|
||||
*
|
||||
* ```
|
||||
* * template T
|
||||
* * return Closure(T): T
|
||||
* function id(): Closure { ... }
|
||||
*
|
||||
* * template A
|
||||
* * template B
|
||||
* *
|
||||
* * param list<A> $_items
|
||||
* * param callable(A): B $_ab
|
||||
* * return list<B>
|
||||
* function map(array $items, callable $ab): array { ... }
|
||||
*
|
||||
* // list<int>
|
||||
* $numbers = [1, 2, 3];
|
||||
*
|
||||
* $result = map($numbers, id());
|
||||
* // $result is list<int> because template T of id() was inferred by previous arg.
|
||||
* ```
|
||||
*/
|
||||
public static function remapLowerBounds(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
TemplateResult $inferred_template_result,
|
||||
HighOrderFunctionArgInfo $input_function,
|
||||
Union $container_function_type
|
||||
): TemplateResult {
|
||||
// Try to infer container callable by $inferred_template_result
|
||||
$container_type = TemplateInferredTypeReplacer::replace(
|
||||
$container_function_type,
|
||||
$inferred_template_result,
|
||||
$statements_analyzer->getCodebase(),
|
||||
);
|
||||
|
||||
$input_function_type = $input_function->getFunctionType();
|
||||
$input_function_template_result = $input_function->getTemplates();
|
||||
|
||||
// Traverse side by side 'container' params and 'input' params.
|
||||
// This maps 'input' templates to 'container' templates.
|
||||
//
|
||||
// Example:
|
||||
// 'input' => Closure(C:Bar, D:Bar): array{C:Bar, D:Bar}
|
||||
// 'container' => Closure(int, string): array{int, string}
|
||||
//
|
||||
// $remapped_lower_bounds will be: [
|
||||
// 'C' => ['Bar' => [int]],
|
||||
// 'D' => ['Bar' => [string]]
|
||||
// ].
|
||||
foreach ($input_function_type->getAtomicTypes() as $input_atomic) {
|
||||
if (!$input_atomic instanceof TClosure && !$input_atomic instanceof TCallable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($container_type->getAtomicTypes() as $container_atomic) {
|
||||
if (!$container_atomic instanceof TClosure && !$container_atomic instanceof TCallable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($input_atomic->params ?? [] as $offset => $input_param) {
|
||||
if (!isset($container_atomic->params[$offset])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TemplateStandinTypeReplacer::fillTemplateResult(
|
||||
$input_param->type ?? Type::getMixed(),
|
||||
$input_function_template_result,
|
||||
$statements_analyzer->getCodebase(),
|
||||
$statements_analyzer,
|
||||
$container_atomic->params[$offset]->type,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $input_function_template_result;
|
||||
}
|
||||
|
||||
public static function enhanceCallableArgType(
|
||||
Context $context,
|
||||
PhpParser\Node\Expr $arg_expr,
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
HighOrderFunctionArgInfo $high_order_callable_info,
|
||||
TemplateResult $high_order_template_result
|
||||
): void {
|
||||
// Psalm can infer simple callable/closure.
|
||||
// But can't infer first-class-callable or high-order function.
|
||||
if ($high_order_callable_info->getType() === HighOrderFunctionArgInfo::TYPE_CALLABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fully_inferred_callable_type = TemplateInferredTypeReplacer::replace(
|
||||
$high_order_callable_info->getFunctionType(),
|
||||
$high_order_template_result,
|
||||
$statements_analyzer->getCodebase(),
|
||||
);
|
||||
|
||||
// Some templates may not have been replaced.
|
||||
// They expansion makes error message better.
|
||||
$expanded = TypeExpander::expandUnion(
|
||||
$statements_analyzer->getCodebase(),
|
||||
$fully_inferred_callable_type,
|
||||
$context->self,
|
||||
$context->self,
|
||||
$context->parent,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
$statements_analyzer->node_data->setType($arg_expr, $expanded);
|
||||
}
|
||||
|
||||
public static function getCallableArgInfo(
|
||||
Context $context,
|
||||
PhpParser\Node\Expr $input_arg_expr,
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
FunctionLikeParameter $container_param
|
||||
): ?HighOrderFunctionArgInfo {
|
||||
if (!self::isSupported($container_param)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
try {
|
||||
if ($input_arg_expr instanceof PhpParser\Node\Expr\FuncCall) {
|
||||
$function_id = strtolower((string) $input_arg_expr->name->getAttribute('resolvedName'));
|
||||
|
||||
if (empty($function_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$dynamic_storage = !$input_arg_expr->isFirstClassCallable()
|
||||
? $codebase->functions->dynamic_storage_provider->getFunctionStorage(
|
||||
$input_arg_expr,
|
||||
$statements_analyzer,
|
||||
$function_id,
|
||||
$context,
|
||||
new CodeLocation($statements_analyzer, $input_arg_expr),
|
||||
)
|
||||
: null;
|
||||
|
||||
return new HighOrderFunctionArgInfo(
|
||||
$input_arg_expr->isFirstClassCallable()
|
||||
? HighOrderFunctionArgInfo::TYPE_FIRST_CLASS_CALLABLE
|
||||
: HighOrderFunctionArgInfo::TYPE_CALLABLE,
|
||||
$dynamic_storage ?? $codebase->functions->getStorage($statements_analyzer, $function_id),
|
||||
);
|
||||
}
|
||||
|
||||
if ($input_arg_expr instanceof PhpParser\Node\Expr\MethodCall &&
|
||||
$input_arg_expr->var instanceof PhpParser\Node\Expr\Variable &&
|
||||
$input_arg_expr->name instanceof PhpParser\Node\Identifier &&
|
||||
is_string($input_arg_expr->var->name) &&
|
||||
isset($context->vars_in_scope['$' . $input_arg_expr->var->name])
|
||||
) {
|
||||
$lhs_type = $context->vars_in_scope['$' . $input_arg_expr->var->name]->getSingleAtomic();
|
||||
|
||||
if (!$lhs_type instanceof Type\Atomic\TNamedObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$method_id = new MethodIdentifier(
|
||||
$lhs_type->value,
|
||||
strtolower((string)$input_arg_expr->name),
|
||||
);
|
||||
|
||||
return new HighOrderFunctionArgInfo(
|
||||
$input_arg_expr->isFirstClassCallable()
|
||||
? HighOrderFunctionArgInfo::TYPE_FIRST_CLASS_CALLABLE
|
||||
: HighOrderFunctionArgInfo::TYPE_CALLABLE,
|
||||
$codebase->methods->getStorage($method_id),
|
||||
);
|
||||
}
|
||||
|
||||
if ($input_arg_expr instanceof PhpParser\Node\Expr\StaticCall &&
|
||||
$input_arg_expr->name instanceof PhpParser\Node\Identifier
|
||||
) {
|
||||
$method_id = new MethodIdentifier(
|
||||
(string)$input_arg_expr->class->getAttribute('resolvedName'),
|
||||
strtolower($input_arg_expr->name->toString()),
|
||||
);
|
||||
|
||||
return new HighOrderFunctionArgInfo(
|
||||
$input_arg_expr->isFirstClassCallable()
|
||||
? HighOrderFunctionArgInfo::TYPE_FIRST_CLASS_CALLABLE
|
||||
: HighOrderFunctionArgInfo::TYPE_CALLABLE,
|
||||
$codebase->methods->getStorage($method_id),
|
||||
);
|
||||
}
|
||||
|
||||
if ($input_arg_expr instanceof PhpParser\Node\Scalar\String_) {
|
||||
return self::fromLiteralString(Type::getString($input_arg_expr->value), $statements_analyzer);
|
||||
}
|
||||
|
||||
if ($input_arg_expr instanceof PhpParser\Node\Expr\ConstFetch) {
|
||||
$constant = $context->constants[$input_arg_expr->name->toString()] ?? null;
|
||||
|
||||
return null !== $constant
|
||||
? self::fromLiteralString($constant, $statements_analyzer)
|
||||
: null;
|
||||
}
|
||||
|
||||
if ($input_arg_expr instanceof PhpParser\Node\Expr\ClassConstFetch &&
|
||||
$input_arg_expr->name instanceof PhpParser\Node\Identifier
|
||||
) {
|
||||
$storage = $codebase->classlikes
|
||||
->getStorageFor((string)$input_arg_expr->class->getAttribute('resolvedName'));
|
||||
|
||||
$constant = null !== $storage
|
||||
? $storage->constants[$input_arg_expr->name->toString()] ?? null
|
||||
: null;
|
||||
|
||||
return null !== $constant && null !== $constant->type
|
||||
? self::fromLiteralString($constant->type, $statements_analyzer)
|
||||
: null;
|
||||
}
|
||||
|
||||
if ($input_arg_expr instanceof PhpParser\Node\Expr\New_ &&
|
||||
$input_arg_expr->class instanceof PhpParser\Node\Name
|
||||
) {
|
||||
$class_storage = $codebase->classlikes
|
||||
->getStorageFor((string) $input_arg_expr->class->getAttribute('resolvedName'));
|
||||
|
||||
$invoke_storage = $class_storage && isset($class_storage->methods['__invoke'])
|
||||
? $class_storage->methods['__invoke']
|
||||
: null;
|
||||
|
||||
if (!$invoke_storage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HighOrderFunctionArgInfo(
|
||||
HighOrderFunctionArgInfo::TYPE_CLASS_CALLABLE,
|
||||
$invoke_storage,
|
||||
$class_storage,
|
||||
);
|
||||
}
|
||||
} catch (UnexpectedValueException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isSupported(FunctionLikeParameter $container_param): bool
|
||||
{
|
||||
if (!$container_param->type || !$container_param->type->hasCallableType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($container_param->type->getAtomicTypes() as $a) {
|
||||
if (($a instanceof TClosure || $a instanceof TCallable) && !$a->params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($a instanceof Type\Atomic\TCallableArray ||
|
||||
$a instanceof Type\Atomic\TCallableString ||
|
||||
$a instanceof Type\Atomic\TCallableKeyedArray
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function fromLiteralString(
|
||||
Union $constant,
|
||||
StatementsAnalyzer $statements_analyzer
|
||||
): ?HighOrderFunctionArgInfo {
|
||||
$literal = $constant->isSingle() ? $constant->getSingleAtomic() : null;
|
||||
|
||||
if (!$literal instanceof Type\Atomic\TLiteralString || empty($literal->value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
return new HighOrderFunctionArgInfo(
|
||||
HighOrderFunctionArgInfo::TYPE_STRING_CALLABLE,
|
||||
strpos($literal->value, '::') !== false
|
||||
? $codebase->methods->getStorage(MethodIdentifier::wrap($literal->value))
|
||||
: $codebase->functions->getStorage($statements_analyzer, strtolower($literal->value)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||||
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\FunctionLikeStorage;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TCallable;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class HighOrderFunctionArgInfo
|
||||
{
|
||||
public const TYPE_FIRST_CLASS_CALLABLE = 'first-class-callable';
|
||||
public const TYPE_CLASS_CALLABLE = 'class-callable';
|
||||
public const TYPE_STRING_CALLABLE = 'string-callable';
|
||||
public const TYPE_CALLABLE = 'callable';
|
||||
|
||||
/** @psalm-var HighOrderFunctionArgInfo::TYPE_* */
|
||||
private string $type;
|
||||
private FunctionLikeStorage $function_storage;
|
||||
private ?ClassLikeStorage $class_storage;
|
||||
|
||||
/**
|
||||
* @psalm-param HighOrderFunctionArgInfo::TYPE_* $type
|
||||
*/
|
||||
public function __construct(
|
||||
string $type,
|
||||
FunctionLikeStorage $function_storage,
|
||||
ClassLikeStorage $class_storage = null
|
||||
) {
|
||||
$this->type = $type;
|
||||
$this->function_storage = $function_storage;
|
||||
$this->class_storage = $class_storage;
|
||||
}
|
||||
|
||||
public function getTemplates(): TemplateResult
|
||||
{
|
||||
$templates = $this->class_storage
|
||||
? array_merge(
|
||||
$this->function_storage->template_types ?? [],
|
||||
$this->class_storage->template_types ?? [],
|
||||
)
|
||||
: $this->function_storage->template_types ?? [];
|
||||
|
||||
return new TemplateResult($templates, []);
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getFunctionType(): Union
|
||||
{
|
||||
switch ($this->type) {
|
||||
case self::TYPE_FIRST_CLASS_CALLABLE:
|
||||
return new Union([
|
||||
new TClosure(
|
||||
'Closure',
|
||||
$this->function_storage->params,
|
||||
$this->function_storage->return_type,
|
||||
$this->function_storage->pure,
|
||||
),
|
||||
]);
|
||||
|
||||
case self::TYPE_STRING_CALLABLE:
|
||||
case self::TYPE_CLASS_CALLABLE:
|
||||
return new Union([
|
||||
new TCallable(
|
||||
'callable',
|
||||
$this->function_storage->params,
|
||||
$this->function_storage->return_type,
|
||||
$this->function_storage->pure,
|
||||
),
|
||||
]);
|
||||
|
||||
default:
|
||||
return $this->function_storage->return_type ?? Type::getMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ use Psalm\StatementsSource;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TCallable;
|
||||
use Psalm\Type\Atomic\TCallableObject;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Atomic\TEmptyMixed;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
@@ -104,6 +106,18 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
|
||||
$source = $statements_analyzer->getSource();
|
||||
|
||||
if ($lhs_type_part instanceof TCallableObject) {
|
||||
self::handleCallableObject(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$context,
|
||||
$lhs_type_part->callable,
|
||||
$result,
|
||||
$inferred_template_result,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$lhs_type_part instanceof TNamedObject) {
|
||||
self::handleInvalidClass(
|
||||
$statements_analyzer,
|
||||
@@ -891,4 +905,55 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
$fq_class_name,
|
||||
];
|
||||
}
|
||||
|
||||
private static function handleCallableObject(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\MethodCall $stmt,
|
||||
Context $context,
|
||||
?TCallable $lhs_type_part_callable,
|
||||
AtomicMethodCallAnalysisResult $result,
|
||||
?TemplateResult $inferred_template_result = null
|
||||
): void {
|
||||
$method_id = 'object::__invoke';
|
||||
$result->existent_method_ids[] = $method_id;
|
||||
$result->has_valid_method_call_type = true;
|
||||
|
||||
if ($lhs_type_part_callable !== null) {
|
||||
$result->return_type = $lhs_type_part_callable->return_type ?? Type::getMixed();
|
||||
$callableArgumentCount = count($lhs_type_part_callable->params ?? []);
|
||||
$providedArgumentsCount = count($stmt->getArgs());
|
||||
|
||||
if ($callableArgumentCount > $providedArgumentsCount) {
|
||||
$result->too_few_arguments = true;
|
||||
$result->too_few_arguments_method_ids[] = new MethodIdentifier('callable-object', '__invoke');
|
||||
} elseif ($providedArgumentsCount > $callableArgumentCount) {
|
||||
$result->too_many_arguments = true;
|
||||
$result->too_many_arguments_method_ids[] = new MethodIdentifier('callable-object', '__invoke');
|
||||
}
|
||||
|
||||
$template_result = $inferred_template_result ?? new TemplateResult([], []);
|
||||
|
||||
ArgumentsAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->getArgs(),
|
||||
$lhs_type_part_callable->params,
|
||||
$method_id,
|
||||
false,
|
||||
$context,
|
||||
$template_result,
|
||||
);
|
||||
|
||||
ArgumentsAnalyzer::checkArgumentsMatch(
|
||||
$statements_analyzer,
|
||||
$stmt->getArgs(),
|
||||
$method_id,
|
||||
$lhs_type_part_callable->params ?? [],
|
||||
null,
|
||||
null,
|
||||
$template_result,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
$context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,13 @@ use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
@@ -225,6 +228,9 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
if ($inferred_template_result) {
|
||||
$template_result->lower_bounds += $inferred_template_result->lower_bounds;
|
||||
}
|
||||
if ($method_storage && $method_storage->template_types) {
|
||||
$template_result->template_types += $method_storage->template_types;
|
||||
}
|
||||
|
||||
if ($codebase->store_node_types
|
||||
&& !$stmt->isFirstClassCallable()
|
||||
@@ -422,30 +428,48 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
|
||||
if ($method_storage->if_true_assertions) {
|
||||
$possibilities = array_map(
|
||||
static fn(Possibilities $assertion): Possibilities => $assertion->getUntemplatedCopy(
|
||||
$template_result,
|
||||
$lhs_var_id,
|
||||
$codebase,
|
||||
),
|
||||
$method_storage->if_true_assertions,
|
||||
);
|
||||
if ($lhs_var_id === null) {
|
||||
$possibilities = array_filter(
|
||||
$possibilities,
|
||||
static fn(Possibilities $assertion): bool => !(is_string($assertion->var_id)
|
||||
&& strpos($assertion->var_id, '$this->') === 0
|
||||
)
|
||||
);
|
||||
}
|
||||
$statements_analyzer->node_data->setIfTrueAssertions(
|
||||
$stmt,
|
||||
array_map(
|
||||
static fn(Possibilities $assertion): Possibilities => $assertion->getUntemplatedCopy(
|
||||
$template_result,
|
||||
$lhs_var_id,
|
||||
$codebase,
|
||||
),
|
||||
$method_storage->if_true_assertions,
|
||||
),
|
||||
$possibilities,
|
||||
);
|
||||
}
|
||||
|
||||
if ($method_storage->if_false_assertions) {
|
||||
$possibilities = array_map(
|
||||
static fn(Possibilities $assertion): Possibilities => $assertion->getUntemplatedCopy(
|
||||
$template_result,
|
||||
$lhs_var_id,
|
||||
$codebase,
|
||||
),
|
||||
$method_storage->if_false_assertions,
|
||||
);
|
||||
if ($lhs_var_id === null) {
|
||||
$possibilities = array_filter(
|
||||
$possibilities,
|
||||
static fn(Possibilities $assertion): bool => !(is_string($assertion->var_id)
|
||||
&& strpos($assertion->var_id, '$this->') === 0
|
||||
)
|
||||
);
|
||||
}
|
||||
$statements_analyzer->node_data->setIfFalseAssertions(
|
||||
$stmt,
|
||||
array_map(
|
||||
static fn(Possibilities $assertion): Possibilities => $assertion->getUntemplatedCopy(
|
||||
$template_result,
|
||||
$lhs_var_id,
|
||||
$codebase,
|
||||
),
|
||||
$method_storage->if_false_assertions,
|
||||
),
|
||||
$possibilities,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -546,7 +570,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 || $codebase->config->seal_all_properties)
|
||||
if (($class_storage->hasSealedProperties($codebase->config))
|
||||
&& !isset($class_storage->pseudo_property_set_types['$' . $prop_name])
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
@@ -644,7 +668,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 || $codebase->config->seal_all_properties)
|
||||
if (($class_storage->hasSealedProperties($codebase->config))
|
||||
&& !isset($class_storage->pseudo_property_get_types['$' . $prop_name])
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
|
||||
@@ -190,7 +190,7 @@ class MissingMethodCallHandler
|
||||
$context,
|
||||
);
|
||||
|
||||
if ($class_storage->sealed_methods || $config->seal_all_methods) {
|
||||
if ($class_storage->hasSealedMethods($config)) {
|
||||
$result->non_existent_magic_method_ids[] = $method_id->__toString();
|
||||
|
||||
return null;
|
||||
|
||||
@@ -52,12 +52,11 @@ use Psalm\Type\Atomic\TObject;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TTemplateParamClass;
|
||||
use Psalm\Type\Atomic\TUnknownClassString;
|
||||
use Psalm\Type\TaintKind;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_shift;
|
||||
use function array_values;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
@@ -74,7 +73,8 @@ class NewAnalyzer extends CallAnalyzer
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\New_ $stmt,
|
||||
Context $context
|
||||
Context $context,
|
||||
TemplateResult $template_result = null
|
||||
): bool {
|
||||
$fq_class_name = null;
|
||||
|
||||
@@ -256,6 +256,7 @@ class NewAnalyzer extends CallAnalyzer
|
||||
$fq_class_name,
|
||||
$from_static,
|
||||
$can_extend,
|
||||
$template_result,
|
||||
);
|
||||
} else {
|
||||
ArgumentsAnalyzer::analyze(
|
||||
@@ -291,7 +292,8 @@ class NewAnalyzer extends CallAnalyzer
|
||||
Context $context,
|
||||
string $fq_class_name,
|
||||
bool $from_static,
|
||||
bool $can_extend
|
||||
bool $can_extend,
|
||||
TemplateResult $template_result = null
|
||||
): void {
|
||||
$storage = $codebase->classlike_storage_provider->get($fq_class_name);
|
||||
|
||||
@@ -392,7 +394,7 @@ class NewAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$template_result = new TemplateResult([], []);
|
||||
$template_result ??= new TemplateResult([], []);
|
||||
|
||||
if (self::checkMethodArgs(
|
||||
$method_id,
|
||||
@@ -699,15 +701,58 @@ class NewAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$new_type = null;
|
||||
$new_type = self::getNewType(
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
$context,
|
||||
$stmt,
|
||||
$stmt_class_type,
|
||||
$config,
|
||||
$can_extend,
|
||||
);
|
||||
|
||||
$stmt_class_types = $stmt_class_type->getAtomicTypes();
|
||||
if (!$has_single_class) {
|
||||
if ($new_type) {
|
||||
$statements_analyzer->node_data->setType($stmt, $new_type);
|
||||
}
|
||||
|
||||
while ($stmt_class_types) {
|
||||
$lhs_type_part = array_shift($stmt_class_types);
|
||||
ArgumentsAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->getArgs(),
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
$context,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
private static function getNewType(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Codebase $codebase,
|
||||
Context $context,
|
||||
PhpParser\Node\Expr\New_ $stmt,
|
||||
Union $stmt_class_type,
|
||||
Config $config,
|
||||
bool &$can_extend
|
||||
): ?Union {
|
||||
$new_types = [];
|
||||
|
||||
foreach ($stmt_class_type->getAtomicTypes() as $lhs_type_part) {
|
||||
if ($lhs_type_part instanceof TTemplateParam) {
|
||||
$stmt_class_types = array_merge($stmt_class_types, $lhs_type_part->as->getAtomicTypes());
|
||||
$as = self::getNewType(
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
$context,
|
||||
$stmt,
|
||||
$lhs_type_part->as,
|
||||
$config,
|
||||
$can_extend,
|
||||
);
|
||||
if ($as) {
|
||||
$new_types []= new Union([$lhs_type_part->replaceAs($as)]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -731,7 +776,7 @@ class NewAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$new_type = Type::combineUnionTypes($new_type, new Union([$new_type_part]));
|
||||
$new_types []= new Union([$new_type_part]);
|
||||
|
||||
if ($lhs_type_part->as_type
|
||||
&& $codebase->classlikes->classExists($lhs_type_part->as_type->value)
|
||||
@@ -777,9 +822,10 @@ class NewAnalyzer extends CallAnalyzer
|
||||
) {
|
||||
if (!$statements_analyzer->node_data->getType($stmt)) {
|
||||
if ($lhs_type_part instanceof TClassString) {
|
||||
$generated_type = $lhs_type_part->as_type
|
||||
? $lhs_type_part->as_type
|
||||
: new TObject();
|
||||
$generated_type = $lhs_type_part->as_type ?? new TObject();
|
||||
if ($lhs_type_part instanceof TUnknownClassString) {
|
||||
$generated_type = $lhs_type_part->as_unknown_type ?? $generated_type;
|
||||
}
|
||||
|
||||
if ($lhs_type_part->as_type
|
||||
&& $codebase->classlikes->classExists($lhs_type_part->as_type->value)
|
||||
@@ -834,7 +880,7 @@ class NewAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$new_type = Type::combineUnionTypes($new_type, new Union([$generated_type]));
|
||||
$new_types []= new Union([$generated_type]);
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -871,7 +917,7 @@ class NewAnalyzer extends CallAnalyzer
|
||||
) {
|
||||
// do nothing
|
||||
} elseif ($lhs_type_part instanceof TNamedObject) {
|
||||
$new_type = Type::combineUnionTypes($new_type, new Union([$lhs_type_part]));
|
||||
$new_types []= new Union([$lhs_type_part]);
|
||||
continue;
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
@@ -884,24 +930,12 @@ class NewAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$new_type = Type::combineUnionTypes($new_type, Type::getObject());
|
||||
$new_types []= Type::getObject();
|
||||
}
|
||||
|
||||
if (!$has_single_class) {
|
||||
if ($new_type) {
|
||||
$statements_analyzer->node_data->setType($stmt, $new_type);
|
||||
}
|
||||
|
||||
ArgumentsAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->getArgs(),
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
$context,
|
||||
);
|
||||
|
||||
return;
|
||||
if ($new_types) {
|
||||
return Type::combineUnionTypeArray($new_types, $codebase);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call\StaticMethod;
|
||||
|
||||
use PhpParser;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Config;
|
||||
@@ -465,7 +466,7 @@ class ExistingAtomicStaticCallAnalyzer
|
||||
private static function getMethodReturnType(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Codebase $codebase,
|
||||
PhpParser\Node\Expr\StaticCall $stmt,
|
||||
StaticCall $stmt,
|
||||
MethodIdentifier $method_id,
|
||||
array $args,
|
||||
TemplateResult $template_result,
|
||||
@@ -493,40 +494,14 @@ class ExistingAtomicStaticCallAnalyzer
|
||||
[$template_type->param_name]
|
||||
[$template_type->defining_class],
|
||||
)) {
|
||||
if ($template_type->param_name === 'TFunctionArgCount') {
|
||||
$template_result->lower_bounds[$template_type->param_name] = [
|
||||
'fn-' . strtolower((string)$method_id) => [
|
||||
new TemplateBound(
|
||||
Type::getInt(false, count($stmt->getArgs())),
|
||||
),
|
||||
],
|
||||
];
|
||||
} elseif ($template_type->param_name === 'TPhpMajorVersion') {
|
||||
$template_result->lower_bounds[$template_type->param_name] = [
|
||||
'fn-' . strtolower((string)$method_id) => [
|
||||
new TemplateBound(
|
||||
Type::getInt(false, $codebase->getMajorAnalysisPhpVersion()),
|
||||
),
|
||||
],
|
||||
];
|
||||
} elseif ($template_type->param_name === 'TPhpVersionId') {
|
||||
$template_result->lower_bounds[$template_type->param_name] = [
|
||||
'fn-' . strtolower((string) $method_id) => [
|
||||
new TemplateBound(
|
||||
Type::getInt(
|
||||
false,
|
||||
$codebase->analysis_php_version_id,
|
||||
),
|
||||
),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$template_result->lower_bounds[$template_type->param_name] = [
|
||||
($template_type->defining_class) => [
|
||||
new TemplateBound(Type::getNever()),
|
||||
],
|
||||
];
|
||||
}
|
||||
$template_result->lower_bounds[$template_type->param_name]
|
||||
= self::resolveTemplateResultLowerBound(
|
||||
$codebase,
|
||||
$stmt,
|
||||
$class_storage,
|
||||
$method_id,
|
||||
$template_type,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,4 +607,68 @@ class ExistingAtomicStaticCallAnalyzer
|
||||
$visitor->traverse($type);
|
||||
return $visitor->matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-array<string,non-empty-list<TemplateBound>>
|
||||
*/
|
||||
private static function resolveTemplateResultLowerBound(
|
||||
Codebase $codebase,
|
||||
StaticCall $stmt,
|
||||
ClassLikeStorage $class_storage,
|
||||
MethodIdentifier $method_id,
|
||||
TTemplateParam $template_type
|
||||
): array {
|
||||
if ($template_type->param_name === 'TFunctionArgCount') {
|
||||
return [
|
||||
'fn-' . strtolower((string)$method_id) => [
|
||||
new TemplateBound(
|
||||
Type::getInt(false, count($stmt->getArgs())),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($template_type->param_name === 'TPhpMajorVersion') {
|
||||
return [
|
||||
'fn-' . strtolower((string)$method_id) => [
|
||||
new TemplateBound(
|
||||
Type::getInt(false, $codebase->getMajorAnalysisPhpVersion()),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($template_type->param_name === 'TPhpVersionId') {
|
||||
return [
|
||||
'fn-' . strtolower((string) $method_id) => [
|
||||
new TemplateBound(
|
||||
Type::getInt(
|
||||
false,
|
||||
$codebase->analysis_php_version_id,
|
||||
),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (isset(
|
||||
$class_storage->template_extended_params[$template_type->defining_class][$template_type->param_name],
|
||||
)) {
|
||||
$extended_param_type = $class_storage->template_extended_params[
|
||||
$template_type->defining_class
|
||||
][$template_type->param_name];
|
||||
|
||||
return [
|
||||
($template_type->defining_class) => [
|
||||
new TemplateBound($extended_param_type),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
($template_type->defining_class) => [
|
||||
new TemplateBound(Type::getNever()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ use Psalm\Storage\Assertion\Truthy;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\Possibilities;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TCallableObject;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TObjectWithProperties;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
@@ -579,6 +580,7 @@ class CallAnalyzer
|
||||
foreach ($type_part->extra_types as $extra_type) {
|
||||
if ($extra_type instanceof TTemplateParam
|
||||
|| $extra_type instanceof TObjectWithProperties
|
||||
|| $extra_type instanceof TCallableObject
|
||||
) {
|
||||
throw new UnexpectedValueException('Shouldn’t get a generic param here');
|
||||
}
|
||||
|
||||
@@ -844,6 +844,7 @@ class ClassConstAnalyzer
|
||||
assert($parent_classlike_storage !== null);
|
||||
if (!isset($parent_classlike_storage->parent_interfaces[strtolower($interface)])
|
||||
&& !isset($interface_storage->parent_interfaces[strtolower($parent_classlike_storage->name)])
|
||||
&& $interface_const_storage !== $parent_const_storage
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new AmbiguousConstantInheritance(
|
||||
|
||||
@@ -26,7 +26,10 @@ class EvalAnalyzer
|
||||
PhpParser\Node\Expr\Eval_ $stmt,
|
||||
Context $context
|
||||
): void {
|
||||
$was_inside_call = $context->inside_call;
|
||||
$context->inside_call = true;
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context);
|
||||
$context->inside_call = $was_inside_call;
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
|
||||
@@ -115,17 +115,31 @@ class AtomicPropertyFetchAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
$has_valid_fetch_type = true;
|
||||
if ($lhs_type_part instanceof TObjectWithProperties) {
|
||||
if (!isset($lhs_type_part->properties[$prop_name])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$has_valid_fetch_type = true;
|
||||
|
||||
if ($lhs_type_part instanceof TObjectWithProperties
|
||||
&& isset($lhs_type_part->properties[$prop_name])
|
||||
) {
|
||||
$stmt_type = $statements_analyzer->node_data->getType($stmt);
|
||||
|
||||
$statements_analyzer->node_data->setType(
|
||||
$stmt,
|
||||
Type::combineUnionTypes(
|
||||
$lhs_type_part->properties[$prop_name],
|
||||
TypeExpander::expandUnion(
|
||||
$statements_analyzer->getCodebase(),
|
||||
$lhs_type_part->properties[$prop_name],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
$stmt_type,
|
||||
),
|
||||
);
|
||||
@@ -133,12 +147,22 @@ class AtomicPropertyFetchAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
$intersection_types = [];
|
||||
if (!$lhs_type_part instanceof TObject) {
|
||||
$intersection_types = $lhs_type_part->getIntersectionTypes();
|
||||
}
|
||||
|
||||
// stdClass and SimpleXMLElement are special cases where we cannot infer the return types
|
||||
// but we don't want to throw an error
|
||||
// Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164
|
||||
if ($lhs_type_part instanceof TObject
|
||||
|| in_array(strtolower($lhs_type_part->value), Config::getInstance()->getUniversalObjectCrates(), true)
|
||||
|| (
|
||||
in_array(strtolower($lhs_type_part->value), Config::getInstance()->getUniversalObjectCrates(), true)
|
||||
&& $intersection_types === []
|
||||
)
|
||||
) {
|
||||
$has_valid_fetch_type = true;
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getMixed());
|
||||
|
||||
return;
|
||||
@@ -149,8 +173,6 @@ class AtomicPropertyFetchAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
$intersection_types = $lhs_type_part->getIntersectionTypes() ?: [];
|
||||
|
||||
$fq_class_name = $lhs_type_part->value;
|
||||
|
||||
$override_property_visibility = false;
|
||||
@@ -193,6 +215,7 @@ class AtomicPropertyFetchAnalyzer
|
||||
|
||||
if ($class_storage->is_enum || in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name))) {
|
||||
if ($prop_name === 'value' && !$class_storage->is_enum) {
|
||||
$has_valid_fetch_type = true;
|
||||
$statements_analyzer->node_data->setType(
|
||||
$stmt,
|
||||
new Union([
|
||||
@@ -201,8 +224,10 @@ class AtomicPropertyFetchAnalyzer
|
||||
]),
|
||||
);
|
||||
} elseif ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) {
|
||||
$has_valid_fetch_type = true;
|
||||
self::handleEnumValue($statements_analyzer, $stmt, $stmt_var_type, $class_storage);
|
||||
} elseif ($prop_name === 'name') {
|
||||
$has_valid_fetch_type = true;
|
||||
self::handleEnumName($statements_analyzer, $stmt, $lhs_type_part);
|
||||
} else {
|
||||
self::handleNonExistentProperty(
|
||||
@@ -220,6 +245,7 @@ class AtomicPropertyFetchAnalyzer
|
||||
$stmt_var_id,
|
||||
$has_magic_getter,
|
||||
$var_id,
|
||||
$has_valid_fetch_type,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,39 +263,60 @@ class AtomicPropertyFetchAnalyzer
|
||||
// add method before changing fq_class_name
|
||||
$get_method_id = new MethodIdentifier($fq_class_name, '__get');
|
||||
|
||||
if (!$naive_property_exists
|
||||
&& $class_storage->namedMixins
|
||||
) {
|
||||
foreach ($class_storage->namedMixins as $mixin) {
|
||||
$new_property_id = $mixin->value . '::$' . $prop_name;
|
||||
if (!$naive_property_exists) {
|
||||
if ($class_storage->namedMixins) {
|
||||
foreach ($class_storage->namedMixins as $mixin) {
|
||||
$new_property_id = $mixin->value . '::$' . $prop_name;
|
||||
|
||||
try {
|
||||
$new_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$new_class_storage = null;
|
||||
}
|
||||
|
||||
if ($new_class_storage
|
||||
&& ($codebase->properties->propertyExists(
|
||||
$new_property_id,
|
||||
!$in_assignment,
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
: null,
|
||||
)
|
||||
|| isset($new_class_storage->pseudo_property_get_types['$' . $prop_name]))
|
||||
) {
|
||||
$fq_class_name = $mixin->value;
|
||||
$lhs_type_part = $mixin;
|
||||
$class_storage = $new_class_storage;
|
||||
|
||||
if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) {
|
||||
$naive_property_exists = true;
|
||||
try {
|
||||
$new_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$new_class_storage = null;
|
||||
}
|
||||
|
||||
$property_id = $new_property_id;
|
||||
if ($new_class_storage
|
||||
&& ($codebase->properties->propertyExists(
|
||||
$new_property_id,
|
||||
!$in_assignment,
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
: null,
|
||||
)
|
||||
|| isset($new_class_storage->pseudo_property_get_types['$' . $prop_name]))
|
||||
) {
|
||||
$fq_class_name = $mixin->value;
|
||||
$lhs_type_part = $mixin;
|
||||
$class_storage = $new_class_storage;
|
||||
|
||||
if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) {
|
||||
$naive_property_exists = true;
|
||||
}
|
||||
|
||||
$property_id = $new_property_id;
|
||||
}
|
||||
}
|
||||
} elseif ($intersection_types !== [] && !$class_storage->final) {
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
self::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$context,
|
||||
$in_assignment,
|
||||
$var_id,
|
||||
$stmt_var_id,
|
||||
$stmt_var_type,
|
||||
$intersection_type,
|
||||
$prop_name,
|
||||
$has_valid_fetch_type,
|
||||
$invalid_fetch_types,
|
||||
$is_static_access,
|
||||
);
|
||||
|
||||
if ($has_valid_fetch_type) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,6 +397,7 @@ class AtomicPropertyFetchAnalyzer
|
||||
$stmt_var_id,
|
||||
$has_magic_getter,
|
||||
$var_id,
|
||||
$has_valid_fetch_type,
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -485,6 +533,8 @@ class AtomicPropertyFetchAnalyzer
|
||||
}
|
||||
|
||||
$stmt_type = $statements_analyzer->node_data->getType($stmt);
|
||||
|
||||
$has_valid_fetch_type = true;
|
||||
$statements_analyzer->node_data->setType(
|
||||
$stmt,
|
||||
Type::combineUnionTypes($class_property_type, $stmt_type),
|
||||
@@ -663,7 +713,7 @@ 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 || $codebase->config->seal_all_properties)
|
||||
if (!($class_storage->hasSealedProperties($codebase->config))
|
||||
&& !$override_property_visibility
|
||||
) {
|
||||
return false;
|
||||
@@ -1144,9 +1194,11 @@ class AtomicPropertyFetchAnalyzer
|
||||
bool $in_assignment,
|
||||
?string $stmt_var_id,
|
||||
bool $has_magic_getter,
|
||||
?string $var_id
|
||||
?string $var_id,
|
||||
bool &$has_valid_fetch_type
|
||||
): void {
|
||||
if ($config->use_phpdoc_property_without_magic_or_parent
|
||||
if (($config->use_phpdoc_property_without_magic_or_parent
|
||||
|| $class_storage->hasAttributeIncludingParents('AllowDynamicProperties', $codebase))
|
||||
&& isset($class_storage->pseudo_property_get_types['$' . $prop_name])
|
||||
) {
|
||||
$stmt_type = $class_storage->pseudo_property_get_types['$' . $prop_name];
|
||||
@@ -1178,6 +1230,7 @@ class AtomicPropertyFetchAnalyzer
|
||||
$context,
|
||||
);
|
||||
|
||||
$has_valid_fetch_type = true;
|
||||
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
||||
|
||||
return;
|
||||
|
||||
@@ -158,7 +158,8 @@ class IncludeAnalyzer
|
||||
|
||||
$current_file_analyzer = $statements_analyzer->getFileAnalyzer();
|
||||
|
||||
if ($current_file_analyzer->project_analyzer->fileExists($path_to_file)) {
|
||||
if ($current_file_analyzer->project_analyzer->fileExists($path_to_file)
|
||||
&& !$current_file_analyzer->project_analyzer->isDirectory($path_to_file)) {
|
||||
if ($statements_analyzer->hasParentFilePath($path_to_file)
|
||||
|| !$codebase->file_storage_provider->has($path_to_file)
|
||||
|| ($statements_analyzer->hasAlreadyRequiredFilePath($path_to_file)
|
||||
@@ -395,6 +396,18 @@ class IncludeAnalyzer
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
if ((substr($file_name, 0, 2) === '.' . DIRECTORY_SEPARATOR)
|
||||
|| (substr($file_name, 0, 3) === '..' . DIRECTORY_SEPARATOR)
|
||||
) {
|
||||
$file = $current_directory . DIRECTORY_SEPARATOR . $file_name;
|
||||
|
||||
if (file_exists($file)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$paths = PATH_SEPARATOR === ':'
|
||||
? preg_split('#(?<!phar):#', get_include_path())
|
||||
: explode(PATH_SEPARATOR, get_include_path());
|
||||
|
||||
@@ -110,10 +110,16 @@ class MatchAnalyzer
|
||||
$stmt->cond->getAttributes(),
|
||||
);
|
||||
}
|
||||
} elseif ($stmt->cond instanceof PhpParser\Node\Expr\FuncCall
|
||||
|| $stmt->cond instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $stmt->cond instanceof PhpParser\Node\Expr\StaticCall
|
||||
} elseif ($stmt->cond instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||
&& $stmt->cond->name instanceof PhpParser\Node\Identifier
|
||||
&& $stmt->cond->name->toString() === 'class'
|
||||
) {
|
||||
// do nothing
|
||||
} elseif ($stmt->cond instanceof PhpParser\Node\Expr\ConstFetch
|
||||
&& $stmt->cond->name->toString() === 'true'
|
||||
) {
|
||||
// do nothing
|
||||
} else {
|
||||
$switch_var_id = '$__tmp_switch__' . (int) $stmt->cond->getAttribute('startFilePos');
|
||||
|
||||
$condition_type = $statements_analyzer->node_data->getType($stmt->cond) ?? Type::getMixed();
|
||||
@@ -128,18 +134,27 @@ class MatchAnalyzer
|
||||
}
|
||||
|
||||
$arms = $stmt->arms;
|
||||
$flattened_arms = [];
|
||||
$last_arm = null;
|
||||
|
||||
foreach ($arms as $i => $arm) {
|
||||
// move default to the end
|
||||
foreach ($arms as $arm) {
|
||||
if ($arm->conds === null) {
|
||||
unset($arms[$i]);
|
||||
$arms[] = $arm;
|
||||
$last_arm = $arm;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($arm->conds as $cond) {
|
||||
$flattened_arms[] = new PhpParser\Node\MatchArm(
|
||||
[$cond],
|
||||
$arm->body,
|
||||
$arm->getAttributes(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$arms = $flattened_arms;
|
||||
$arms = array_reverse($arms);
|
||||
|
||||
$last_arm = array_shift($arms);
|
||||
$last_arm ??= array_shift($arms);
|
||||
|
||||
if (!$last_arm) {
|
||||
IssueBuffer::maybeAdd(
|
||||
|
||||
@@ -666,17 +666,22 @@ class SimpleTypeInferer
|
||||
} elseif ($key_type->isSingleIntLiteral()) {
|
||||
$item_key_value = $key_type->getSingleIntLiteral()->value;
|
||||
|
||||
if ($item_key_value >= $array_creation_info->int_offset) {
|
||||
if ($item_key_value === $array_creation_info->int_offset) {
|
||||
if ($item_key_value <= PHP_INT_MAX
|
||||
&& $item_key_value > $array_creation_info->int_offset
|
||||
) {
|
||||
if ($item_key_value - 1 === $array_creation_info->int_offset) {
|
||||
$item_is_list_item = true;
|
||||
}
|
||||
$array_creation_info->int_offset = $item_key_value + 1;
|
||||
$array_creation_info->int_offset = $item_key_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($array_creation_info->int_offset === PHP_INT_MAX) {
|
||||
return false;
|
||||
}
|
||||
$item_is_list_item = true;
|
||||
$item_key_value = $array_creation_info->int_offset++;
|
||||
$item_key_value = ++$array_creation_info->int_offset;
|
||||
$array_creation_info->item_key_atomic_types[] = new TLiteralInt($item_key_value);
|
||||
}
|
||||
|
||||
@@ -760,7 +765,10 @@ class SimpleTypeInferer
|
||||
$new_offset = $key;
|
||||
$array_creation_info->item_key_atomic_types[] = Type::getAtomicStringFromLiteral($new_offset);
|
||||
} else {
|
||||
$new_offset = $array_creation_info->int_offset++;
|
||||
if ($array_creation_info->int_offset === PHP_INT_MAX) {
|
||||
return false;
|
||||
}
|
||||
$new_offset = ++$array_creation_info->int_offset;
|
||||
$array_creation_info->item_key_atomic_types[] = new TLiteralInt($new_offset);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ use Psalm\Issue\UnrecognizedExpression;
|
||||
use Psalm\Issue\UnsupportedReferenceUsage;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\BeforeExpressionAnalysisEvent;
|
||||
use Psalm\Storage\FunctionLikeParameter;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\TaintKind;
|
||||
@@ -80,6 +81,10 @@ class ExpressionAnalyzer
|
||||
?TemplateResult $template_result = null,
|
||||
bool $assigned_to_reference = false
|
||||
): bool {
|
||||
if (self::dispatchBeforeExpressionAnalysis($stmt, $context, $statements_analyzer) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if (self::handleExpression(
|
||||
@@ -126,24 +131,10 @@ class ExpressionAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$event = new AfterExpressionAnalysisEvent(
|
||||
$stmt,
|
||||
$context,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
[],
|
||||
);
|
||||
|
||||
if ($codebase->config->eventDispatcher->dispatchAfterExpressionAnalysis($event) === false) {
|
||||
if (self::dispatchAfterExpressionAnalysis($stmt, $context, $statements_analyzer) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file_manipulations = $event->getFileReplacements();
|
||||
|
||||
if ($file_manipulations) {
|
||||
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -280,7 +271,7 @@ class ExpressionAnalyzer
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\New_) {
|
||||
return NewAnalyzer::analyze($statements_analyzer, $stmt, $context);
|
||||
return NewAnalyzer::analyze($statements_analyzer, $stmt, $context, $template_result);
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\Array_) {
|
||||
@@ -554,4 +545,60 @@ class ExpressionAnalyzer
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function dispatchBeforeExpressionAnalysis(
|
||||
PhpParser\Node\Expr $expr,
|
||||
Context $context,
|
||||
StatementsAnalyzer $statements_analyzer
|
||||
): ?bool {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$event = new BeforeExpressionAnalysisEvent(
|
||||
$expr,
|
||||
$context,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
[],
|
||||
);
|
||||
|
||||
if ($codebase->config->eventDispatcher->dispatchBeforeExpressionAnalysis($event) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file_manipulations = $event->getFileReplacements();
|
||||
|
||||
if ($file_manipulations !== []) {
|
||||
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function dispatchAfterExpressionAnalysis(
|
||||
PhpParser\Node\Expr $expr,
|
||||
Context $context,
|
||||
StatementsAnalyzer $statements_analyzer
|
||||
): ?bool {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$event = new AfterExpressionAnalysisEvent(
|
||||
$expr,
|
||||
$context,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
[],
|
||||
);
|
||||
|
||||
if ($codebase->config->eventDispatcher->dispatchAfterExpressionAnalysis($event) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file_manipulations = $event->getFileReplacements();
|
||||
|
||||
if ($file_manipulations !== []) {
|
||||
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +496,7 @@ class StatementsAnalyzer extends SourceAnalyzer
|
||||
&& !($stmt instanceof PhpParser\Node\Stmt\Interface_)
|
||||
&& !($stmt instanceof PhpParser\Node\Stmt\Trait_)
|
||||
&& !($stmt instanceof PhpParser\Node\Stmt\HaltCompiler)
|
||||
&& !($stmt instanceof PhpParser\Node\Stmt\Declare_)
|
||||
) {
|
||||
if ($codebase->find_unused_variables) {
|
||||
IssueBuffer::maybeAdd(
|
||||
|
||||
@@ -2,20 +2,14 @@
|
||||
|
||||
namespace Psalm\Internal\Cli;
|
||||
|
||||
use LanguageServerProtocol\MessageType;
|
||||
use Psalm\Config;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Internal\CliUtils;
|
||||
use Psalm\Internal\Composer;
|
||||
use Psalm\Internal\ErrorHandler;
|
||||
use Psalm\Internal\Fork\PsalmRestarter;
|
||||
use Psalm\Internal\IncludeCollector;
|
||||
use Psalm\Internal\Provider\ClassLikeStorageCacheProvider;
|
||||
use Psalm\Internal\Provider\FileProvider;
|
||||
use Psalm\Internal\Provider\FileReferenceCacheProvider;
|
||||
use Psalm\Internal\Provider\FileStorageCacheProvider;
|
||||
use Psalm\Internal\Provider\ParserCacheProvider;
|
||||
use Psalm\Internal\Provider\ProjectCacheProvider;
|
||||
use Psalm\Internal\Provider\Providers;
|
||||
use Psalm\Internal\LanguageServer\ClientConfiguration;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer as LanguageServerLanguageServer;
|
||||
use Psalm\Report;
|
||||
|
||||
use function array_key_exists;
|
||||
@@ -52,16 +46,21 @@ require_once __DIR__ . '/../ErrorHandler.php';
|
||||
require_once __DIR__ . '/../CliUtils.php';
|
||||
require_once __DIR__ . '/../Composer.php';
|
||||
require_once __DIR__ . '/../IncludeCollector.php';
|
||||
require_once __DIR__ . '/../LanguageServer/ClientConfiguration.php';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class LanguageServer
|
||||
{
|
||||
/** @param array<int,string> $argv */
|
||||
/**
|
||||
* @param array<int,string> $argv
|
||||
* @psalm-suppress ComplexMethod
|
||||
*/
|
||||
public static function run(array $argv): void
|
||||
{
|
||||
CliUtils::checkRuntimeRequirements();
|
||||
$clientConfiguration = new ClientConfiguration();
|
||||
gc_disable();
|
||||
ErrorHandler::install($argv);
|
||||
$valid_short_options = [
|
||||
@@ -72,7 +71,6 @@ final class LanguageServer
|
||||
];
|
||||
|
||||
$valid_long_options = [
|
||||
'clear-cache',
|
||||
'config:',
|
||||
'find-dead-code',
|
||||
'help',
|
||||
@@ -82,7 +80,17 @@ final class LanguageServer
|
||||
'tcp:',
|
||||
'tcp-server',
|
||||
'disable-on-change::',
|
||||
'use-baseline:',
|
||||
'enable-autocomplete::',
|
||||
'enable-code-actions::',
|
||||
'enable-provide-diagnostics::',
|
||||
'enable-provide-hover::',
|
||||
'enable-provide-signature-help::',
|
||||
'enable-provide-definition::',
|
||||
'show-diagnostic-warnings::',
|
||||
'in-memory::',
|
||||
'disable-xdebug::',
|
||||
'on-change-debounce-ms::',
|
||||
'use-extended-diagnostic-codes',
|
||||
'verbose',
|
||||
];
|
||||
@@ -164,12 +172,12 @@ final class LanguageServer
|
||||
--find-dead-code
|
||||
Look for dead code
|
||||
|
||||
--clear-cache
|
||||
Clears all cache files that the language server uses for this specific project
|
||||
|
||||
--use-ini-defaults
|
||||
Use PHP-provided ini defaults for memory and error display
|
||||
|
||||
--use-baseline=PATH
|
||||
Allows you to use a baseline other than the default baseline provided in your config
|
||||
|
||||
--tcp=url
|
||||
Use TCP mode (by default Psalm uses STDIO)
|
||||
|
||||
@@ -180,12 +188,39 @@ final class LanguageServer
|
||||
If added, the language server will not respond to onChange events.
|
||||
You can also specify a line count over which Psalm will not run on-change events.
|
||||
|
||||
--enable-code-actions[=BOOL]
|
||||
Enables or disables code actions. Default is true.
|
||||
|
||||
--enable-provide-diagnostics[=BOOL]
|
||||
Enables or disables providing diagnostics. Default is true.
|
||||
|
||||
--enable-autocomplete[=BOOL]
|
||||
Enables or disables autocomplete on methods and properties. Default is true.
|
||||
|
||||
--use-extended-diagnostic-codes
|
||||
--enable-provide-hover[=BOOL]
|
||||
Enables or disables providing hover. Default is true.
|
||||
|
||||
--enable-provide-signature-help[=BOOL]
|
||||
Enables or disables providing signature help. Default is true.
|
||||
|
||||
--enable-provide-definition[=BOOL]
|
||||
Enables or disables providing definition. Default is true.
|
||||
|
||||
--show-diagnostic-warnings[=BOOL]
|
||||
Enables or disables showing diagnostic warnings. Default is true.
|
||||
|
||||
--use-extended-diagnostic-codes (DEPRECATED)
|
||||
Enables sending help uri links with the code in diagnostic messages.
|
||||
|
||||
--on-change-debounce-ms=[INT]
|
||||
The number of milliseconds to debounce onChange events.
|
||||
|
||||
--disable-xdebug[=BOOL]
|
||||
Disable xdebug for performance reasons. Enable for debugging
|
||||
|
||||
--in-memory[=BOOL]
|
||||
Use in-memory mode. Default is false. Experimental.
|
||||
|
||||
--verbose
|
||||
Will send log messages to the client with information.
|
||||
|
||||
@@ -245,8 +280,14 @@ final class LanguageServer
|
||||
'blackfire',
|
||||
]);
|
||||
|
||||
// If Xdebug is enabled, restart without it
|
||||
$ini_handler->check();
|
||||
$disableXdebug = !isset($options['disable-xdebug'])
|
||||
|| !is_string($options['disable-xdebug'])
|
||||
|| strtolower($options['disable-xdebug']) !== 'false';
|
||||
|
||||
// If Xdebug is enabled, restart without it based on cli
|
||||
if ($disableXdebug) {
|
||||
$ini_handler->check();
|
||||
}
|
||||
|
||||
setlocale(LC_CTYPE, 'C');
|
||||
|
||||
@@ -259,8 +300,6 @@ final class LanguageServer
|
||||
}
|
||||
}
|
||||
|
||||
$find_unused_code = isset($options['find-dead-code']) ? 'auto' : null;
|
||||
|
||||
$config = CliUtils::initializeConfig(
|
||||
$path_to_config,
|
||||
$current_dir,
|
||||
@@ -276,58 +315,85 @@ final class LanguageServer
|
||||
|
||||
$config->setServerMode();
|
||||
|
||||
if (isset($options['clear-cache'])) {
|
||||
$inMemory = isset($options['in-memory']) &&
|
||||
is_string($options['in-memory']) &&
|
||||
strtolower($options['in-memory']) === 'true';
|
||||
|
||||
if ($inMemory) {
|
||||
$config->cache_directory = null;
|
||||
} else {
|
||||
$cache_directory = $config->getCacheDirectory();
|
||||
|
||||
if ($cache_directory !== null) {
|
||||
Config::removeCacheDirectory($cache_directory);
|
||||
}
|
||||
echo 'Cache directory deleted' . PHP_EOL;
|
||||
exit;
|
||||
}
|
||||
|
||||
$providers = new Providers(
|
||||
new FileProvider,
|
||||
new ParserCacheProvider($config),
|
||||
new FileStorageCacheProvider($config),
|
||||
new ClassLikeStorageCacheProvider($config),
|
||||
new FileReferenceCacheProvider($config),
|
||||
new ProjectCacheProvider(Composer::getLockFilePath($current_dir)),
|
||||
);
|
||||
|
||||
$project_analyzer = new ProjectAnalyzer(
|
||||
$config,
|
||||
$providers,
|
||||
);
|
||||
|
||||
if ($config->find_unused_variables) {
|
||||
$project_analyzer->getCodebase()->reportUnusedVariables();
|
||||
}
|
||||
|
||||
if ($config->find_unused_code) {
|
||||
$find_unused_code = 'auto';
|
||||
if (isset($options['use-baseline']) && is_string($options['use-baseline'])) {
|
||||
$clientConfiguration->baseline = $options['use-baseline'];
|
||||
}
|
||||
|
||||
if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) {
|
||||
$project_analyzer->onchange_line_limit = (int) $options['disable-on-change'];
|
||||
$clientConfiguration->onchangeLineLimit = (int) $options['disable-on-change'];
|
||||
}
|
||||
|
||||
$project_analyzer->provide_completion = !isset($options['enable-autocomplete'])
|
||||
if (isset($options['on-change-debounce-ms']) && is_numeric($options['on-change-debounce-ms'])) {
|
||||
$clientConfiguration->onChangeDebounceMs = (int) $options['on-change-debounce-ms'];
|
||||
}
|
||||
|
||||
$clientConfiguration->provideDefinition = !isset($options['enable-provide-definition'])
|
||||
|| !is_string($options['enable-provide-definition'])
|
||||
|| strtolower($options['enable-provide-definition']) !== 'false';
|
||||
|
||||
$clientConfiguration->provideSignatureHelp = !isset($options['enable-provide-signature-help'])
|
||||
|| !is_string($options['enable-provide-signature-help'])
|
||||
|| strtolower($options['enable-provide-signature-help']) !== 'false';
|
||||
|
||||
$clientConfiguration->provideHover = !isset($options['enable-provide-hover'])
|
||||
|| !is_string($options['enable-provide-hover'])
|
||||
|| strtolower($options['enable-provide-hover']) !== 'false';
|
||||
|
||||
$clientConfiguration->provideDiagnostics = !isset($options['enable-provide-diagnostics'])
|
||||
|| !is_string($options['enable-provide-diagnostics'])
|
||||
|| strtolower($options['enable-provide-diagnostics']) !== 'false';
|
||||
|
||||
$clientConfiguration->provideCodeActions = !isset($options['enable-code-actions'])
|
||||
|| !is_string($options['enable-code-actions'])
|
||||
|| strtolower($options['enable-code-actions']) !== 'false';
|
||||
|
||||
$clientConfiguration->provideCompletion = !isset($options['enable-autocomplete'])
|
||||
|| !is_string($options['enable-autocomplete'])
|
||||
|| strtolower($options['enable-autocomplete']) !== 'false';
|
||||
|
||||
if ($find_unused_code) {
|
||||
$project_analyzer->getCodebase()->reportUnusedCode($find_unused_code);
|
||||
}
|
||||
$clientConfiguration->hideWarnings = !(
|
||||
!isset($options['show-diagnostic-warnings'])
|
||||
|| !is_string($options['show-diagnostic-warnings'])
|
||||
|| strtolower($options['show-diagnostic-warnings']) !== 'false'
|
||||
);
|
||||
|
||||
if (isset($options['use-extended-diagnostic-codes'])) {
|
||||
$project_analyzer->language_server_use_extended_diagnostic_codes = true;
|
||||
/**
|
||||
* if ($config->find_unused_variables) {
|
||||
* $project_analyzer->getCodebase()->reportUnusedVariables();
|
||||
* }
|
||||
*/
|
||||
|
||||
$find_unused_code = isset($options['find-dead-code']) ? 'auto' : null;
|
||||
if ($config->find_unused_code) {
|
||||
$find_unused_code = 'auto';
|
||||
}
|
||||
if ($find_unused_code) {
|
||||
$clientConfiguration->findUnusedCode = $find_unused_code;
|
||||
}
|
||||
|
||||
if (isset($options['verbose'])) {
|
||||
$project_analyzer->language_server_verbose = true;
|
||||
$clientConfiguration->logLevel = $options['verbose'] ? MessageType::LOG : MessageType::INFO;
|
||||
} else {
|
||||
$clientConfiguration->logLevel = MessageType::INFO;
|
||||
}
|
||||
|
||||
$project_analyzer->server($options['tcp'] ?? null, isset($options['tcp-server']));
|
||||
$clientConfiguration->TCPServerAddress = $options['tcp'] ?? null;
|
||||
$clientConfiguration->TCPServerMode = isset($options['tcp-server']);
|
||||
|
||||
LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir, $inMemory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,15 +57,12 @@ use function getopt;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function ini_get;
|
||||
use function ini_set;
|
||||
use function is_array;
|
||||
use function is_numeric;
|
||||
use function is_scalar;
|
||||
use function is_string;
|
||||
use function json_encode;
|
||||
use function max;
|
||||
use function microtime;
|
||||
use function opcache_get_status;
|
||||
use function parse_url;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
@@ -190,7 +187,7 @@ final class Psalm
|
||||
|
||||
self::validateCliArguments($args);
|
||||
|
||||
self::setMemoryLimit($options);
|
||||
CliUtils::setMemoryLimit($options);
|
||||
|
||||
self::syncShortOptions($options);
|
||||
|
||||
@@ -463,29 +460,6 @@ final class Psalm
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string|false|list<mixed>> $options
|
||||
*/
|
||||
private static function setMemoryLimit(array $options): void
|
||||
{
|
||||
if (!array_key_exists('use-ini-defaults', $options)) {
|
||||
ini_set('display_errors', 'stderr');
|
||||
ini_set('display_startup_errors', '1');
|
||||
|
||||
$memoryLimit = (8 * 1_024 * 1_024 * 1_024);
|
||||
|
||||
if (array_key_exists('memory-limit', $options)) {
|
||||
$memoryLimit = $options['memory-limit'];
|
||||
|
||||
if (!is_scalar($memoryLimit)) {
|
||||
throw new ConfigException('Invalid memory limit specified.');
|
||||
}
|
||||
}
|
||||
|
||||
ini_set('memory_limit', (string) $memoryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $args
|
||||
*/
|
||||
@@ -923,11 +897,7 @@ final class Psalm
|
||||
// If Xdebug is enabled, restart without it
|
||||
$ini_handler->check();
|
||||
|
||||
if (!function_exists('opcache_get_status')
|
||||
|| !($opcache_status = opcache_get_status(false))
|
||||
|| !isset($opcache_status['opcache_enabled'])
|
||||
|| !$opcache_status['opcache_enabled']
|
||||
) {
|
||||
if (!function_exists('opcache_get_status')) {
|
||||
$progress->write(PHP_EOL
|
||||
. 'Install the opcache extension to make use of JIT on PHP 8.0+ for a 20%+ performance boost!'
|
||||
. PHP_EOL . PHP_EOL);
|
||||
@@ -1273,6 +1243,9 @@ final class Psalm
|
||||
--php-version=PHP_VERSION
|
||||
Explicitly set PHP version to analyse code against.
|
||||
|
||||
--error-level=ERROR_LEVEL
|
||||
Set the error reporting level
|
||||
|
||||
Surfacing issues:
|
||||
--show-info[=BOOLEAN]
|
||||
Show non-exception parser findings (defaults to false).
|
||||
|
||||
@@ -43,7 +43,6 @@ use function getcwd;
|
||||
use function getopt;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function ini_set;
|
||||
use function is_array;
|
||||
use function is_dir;
|
||||
use function is_numeric;
|
||||
@@ -89,6 +88,7 @@ final class Psalter
|
||||
'add-newline-between-docblock-annotations:',
|
||||
'no-cache',
|
||||
'no-progress',
|
||||
'memory-limit:',
|
||||
];
|
||||
|
||||
/** @param array<int,string> $argv */
|
||||
@@ -100,8 +100,6 @@ final class Psalter
|
||||
|
||||
ErrorHandler::install($argv);
|
||||
|
||||
self::setMemoryLimit();
|
||||
|
||||
$args = array_slice($argv, 1);
|
||||
|
||||
// get options from command line
|
||||
@@ -109,6 +107,8 @@ final class Psalter
|
||||
|
||||
self::validateCliArguments($args);
|
||||
|
||||
CliUtils::setMemoryLimit($options);
|
||||
|
||||
self::syncShortOptions($options);
|
||||
|
||||
if (isset($options['c']) && is_array($options['c'])) {
|
||||
@@ -442,15 +442,6 @@ final class Psalter
|
||||
IssueBuffer::finish($project_analyzer, false, $start_time);
|
||||
}
|
||||
|
||||
private static function setMemoryLimit(): void
|
||||
{
|
||||
$memLimit = CliUtils::getMemoryLimitInBytes();
|
||||
// Magic number is 4096M in bytes
|
||||
if ($memLimit > 0 && $memLimit < 8 * 1_024 * 1_024 * 1_024) {
|
||||
ini_set('memory_limit', (string) (8 * 1_024 * 1_024 * 1_024));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int,string> $args */
|
||||
private static function validateCliArguments(array $args): void
|
||||
{
|
||||
|
||||
@@ -14,8 +14,8 @@ use Psalm\Report;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_key_exists;
|
||||
use function array_slice;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function define;
|
||||
use function dirname;
|
||||
@@ -27,13 +27,13 @@ use function file_put_contents;
|
||||
use function fwrite;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function ini_get;
|
||||
use function ini_set;
|
||||
use function is_array;
|
||||
use function is_dir;
|
||||
use function is_scalar;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use function preg_last_error_msg;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function preg_split;
|
||||
use function realpath;
|
||||
@@ -41,7 +41,6 @@ use function stream_get_meta_data;
|
||||
use function stream_set_blocking;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strtoupper;
|
||||
use function substr;
|
||||
use function substr_replace;
|
||||
use function trim;
|
||||
@@ -446,38 +445,27 @@ final class CliUtils
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @param array<string,string|false|list<mixed>> $options
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public static function getMemoryLimitInBytes(): int
|
||||
public static function setMemoryLimit(array $options): void
|
||||
{
|
||||
return self::convertMemoryLimitToBytes(ini_get('memory_limit'));
|
||||
}
|
||||
if (!array_key_exists('use-ini-defaults', $options)) {
|
||||
ini_set('display_errors', 'stderr');
|
||||
ini_set('display_startup_errors', '1');
|
||||
|
||||
/** @psalm-pure */
|
||||
public static function convertMemoryLimitToBytes(string $limit): int
|
||||
{
|
||||
// for unlimited = -1
|
||||
if ($limit < 0) {
|
||||
return -1;
|
||||
}
|
||||
$memoryLimit = (8 * 1_024 * 1_024 * 1_024);
|
||||
|
||||
if (preg_match('/^(\d+)(\D?)$/', $limit, $matches)) {
|
||||
assert(isset($matches[1]));
|
||||
$limit = (int)$matches[1];
|
||||
switch (strtoupper($matches[2] ?? '')) {
|
||||
case 'G':
|
||||
$limit *= 1_024 * 1_024 * 1_024;
|
||||
break;
|
||||
case 'M':
|
||||
$limit *= 1_024 * 1_024;
|
||||
break;
|
||||
case 'K':
|
||||
$limit *= 1_024;
|
||||
break;
|
||||
if (array_key_exists('memory-limit', $options)) {
|
||||
$memoryLimit = $options['memory-limit'];
|
||||
|
||||
if (!is_scalar($memoryLimit)) {
|
||||
throw new ConfigException('Invalid memory limit specified.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (int)$limit;
|
||||
ini_set('memory_limit', (string) $memoryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public static function initPhpVersion(array $options, Config $config, ProjectAnalyzer $project_analyzer): void
|
||||
|
||||
@@ -9,25 +9,23 @@ use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ClassConstantByWildcardResolver
|
||||
{
|
||||
private StorageByPatternResolver $resolver;
|
||||
private Codebase $codebase;
|
||||
|
||||
public function __construct(Codebase $codebase)
|
||||
{
|
||||
$this->resolver = new StorageByPatternResolver();
|
||||
$this->codebase = $codebase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Atomic>|null
|
||||
* @return non-empty-array<array-key,Atomic>|null
|
||||
*/
|
||||
public function resolve(string $class_name, string $constant_pattern): ?array
|
||||
{
|
||||
@@ -35,24 +33,27 @@ final class ClassConstantByWildcardResolver
|
||||
return null;
|
||||
}
|
||||
|
||||
$constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern));
|
||||
$classlike_storage = $this->codebase->classlike_storage_provider->get($class_name);
|
||||
|
||||
$class_like_storage = $this->codebase->classlike_storage_provider->get($class_name);
|
||||
$matched_class_constant_types = [];
|
||||
|
||||
foreach ($class_like_storage->constants as $constant => $class_constant_storage) {
|
||||
if (preg_match($constant_regex_pattern, $constant) === 0) {
|
||||
continue;
|
||||
}
|
||||
$constants = $this->resolver->resolveConstants(
|
||||
$classlike_storage,
|
||||
$constant_pattern,
|
||||
);
|
||||
|
||||
$types = [];
|
||||
foreach ($constants as $class_constant_storage) {
|
||||
if (! $class_constant_storage->type) {
|
||||
$matched_class_constant_types[] = [new TMixed()];
|
||||
$types[] = [new TMixed()];
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched_class_constant_types[] = $class_constant_storage->type->getAtomicTypes();
|
||||
$types[] = $class_constant_storage->type->getAtomicTypes();
|
||||
}
|
||||
|
||||
return array_values(array_merge([], ...$matched_class_constant_types));
|
||||
if ($types === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_merge([], ...$types);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ use ReflectionProperty;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
@@ -1603,8 +1604,7 @@ class ClassLikes
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE
|
||||
* $visibility
|
||||
* @param ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE $visibility
|
||||
*/
|
||||
public function getClassConstantType(
|
||||
string $class_name,
|
||||
@@ -1612,7 +1612,8 @@ class ClassLikes
|
||||
int $visibility,
|
||||
?StatementsAnalyzer $statements_analyzer = null,
|
||||
array $visited_constant_ids = [],
|
||||
bool $late_static_binding = false
|
||||
bool $late_static_binding = false,
|
||||
bool $in_value_of_context = false
|
||||
): ?Union {
|
||||
$class_name = strtolower($class_name);
|
||||
|
||||
@@ -1622,41 +1623,42 @@ class ClassLikes
|
||||
|
||||
$storage = $this->classlike_storage_provider->get($class_name);
|
||||
|
||||
if (isset($storage->constants[$constant_name])) {
|
||||
$constant_storage = $storage->constants[$constant_name];
|
||||
$enum_types = null;
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
) {
|
||||
return null;
|
||||
if ($storage->is_enum) {
|
||||
$enum_types = $this->getEnumType(
|
||||
$storage,
|
||||
$constant_name,
|
||||
);
|
||||
|
||||
if ($in_value_of_context) {
|
||||
return $enum_types;
|
||||
}
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PROTECTED
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PROTECTED
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($constant_storage->unresolved_node) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve(
|
||||
$this,
|
||||
$constant_storage->unresolved_node,
|
||||
$statements_analyzer,
|
||||
$visited_constant_ids,
|
||||
)]);
|
||||
if ($constant_storage->type === null || !$constant_storage->type->from_docblock) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->type = $constant_storage->inferred_type;
|
||||
}
|
||||
}
|
||||
|
||||
return $late_static_binding ? $constant_storage->type : ($constant_storage->inferred_type ?? null);
|
||||
} elseif (isset($storage->enum_cases[$constant_name])) {
|
||||
return new Union([new TEnumCase($storage->name, $constant_name)]);
|
||||
}
|
||||
return null;
|
||||
|
||||
$constant_types = $this->getConstantType(
|
||||
$storage,
|
||||
$constant_name,
|
||||
$visibility,
|
||||
$statements_analyzer,
|
||||
$visited_constant_ids,
|
||||
$late_static_binding,
|
||||
);
|
||||
|
||||
$types = [];
|
||||
if ($enum_types !== null) {
|
||||
$types = array_merge($types, $enum_types->getAtomicTypes());
|
||||
}
|
||||
|
||||
if ($constant_types !== null) {
|
||||
$types = array_merge($types, $constant_types->getAtomicTypes());
|
||||
}
|
||||
|
||||
if ($types === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Union($types);
|
||||
}
|
||||
|
||||
private function checkMethodReferences(ClassLikeStorage $classlike_storage, Methods $methods): void
|
||||
@@ -2366,4 +2368,113 @@ class ClassLikes
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function getConstantType(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $constant_name,
|
||||
int $visibility,
|
||||
?StatementsAnalyzer $statements_analyzer,
|
||||
array $visited_constant_ids,
|
||||
bool $late_static_binding
|
||||
): ?Union {
|
||||
$constant_resolver = new StorageByPatternResolver();
|
||||
$resolved_constants = $constant_resolver->resolveConstants(
|
||||
$class_like_storage,
|
||||
$constant_name,
|
||||
);
|
||||
|
||||
$filtered_constants_by_visibility = array_filter(
|
||||
$resolved_constants,
|
||||
fn(ClassConstantStorage $resolved_constant) => $this->filterConstantNameByVisibility(
|
||||
$resolved_constant,
|
||||
$visibility,
|
||||
)
|
||||
);
|
||||
|
||||
if ($filtered_constants_by_visibility === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$new_atomic_types = [];
|
||||
|
||||
foreach ($filtered_constants_by_visibility as $filtered_constant_name => $constant_storage) {
|
||||
if (!isset($class_like_storage->constants[$filtered_constant_name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($constant_storage->unresolved_node) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve(
|
||||
$this,
|
||||
$constant_storage->unresolved_node,
|
||||
$statements_analyzer,
|
||||
$visited_constant_ids,
|
||||
)]);
|
||||
|
||||
if ($constant_storage->type === null || !$constant_storage->type->from_docblock) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->type = $constant_storage->inferred_type;
|
||||
}
|
||||
}
|
||||
|
||||
$constant_type = $late_static_binding
|
||||
? $constant_storage->type
|
||||
: ($constant_storage->inferred_type ?? null);
|
||||
|
||||
if ($constant_type === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_atomic_types[] = $constant_type->getAtomicTypes();
|
||||
}
|
||||
|
||||
if ($new_atomic_types === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Union(array_merge([], ...$new_atomic_types));
|
||||
}
|
||||
|
||||
private function getEnumType(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $constant_name
|
||||
): ?Union {
|
||||
$constant_resolver = new StorageByPatternResolver();
|
||||
$resolved_enums = $constant_resolver->resolveEnums(
|
||||
$class_like_storage,
|
||||
$constant_name,
|
||||
);
|
||||
|
||||
if ($resolved_enums === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$types = [];
|
||||
foreach (array_keys($resolved_enums) as $enum_case_name) {
|
||||
$types[$enum_case_name] = new TEnumCase($class_like_storage->name, $enum_case_name);
|
||||
}
|
||||
|
||||
return new Union($types);
|
||||
}
|
||||
|
||||
private function filterConstantNameByVisibility(
|
||||
ClassConstantStorage $constant_storage,
|
||||
int $visibility
|
||||
): bool {
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PROTECTED
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PROTECTED
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ use Psalm\Internal\Scanner\UnresolvedConstant\ArraySpread;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\ArrayValue;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\ClassConstant;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\Constant;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\EnumNameFetch;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\EnumPropertyFetch;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\EnumValueFetch;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\ScalarValue;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\UnresolvedAdditionOp;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\UnresolvedBinaryOp;
|
||||
@@ -331,6 +334,24 @@ class ConstantTypeResolver
|
||||
}
|
||||
}
|
||||
|
||||
if ($c instanceof EnumPropertyFetch) {
|
||||
if ($classlikes->enumExists($c->fqcln)) {
|
||||
$enum_storage = $classlikes->getStorageFor($c->fqcln);
|
||||
if (isset($enum_storage->enum_cases[$c->case])) {
|
||||
if ($c instanceof EnumValueFetch) {
|
||||
$value = $enum_storage->enum_cases[$c->case]->value;
|
||||
if (is_string($value)) {
|
||||
return Type::getString($value)->getSingleAtomic();
|
||||
} elseif (is_int($value)) {
|
||||
return Type::getInt(false, $value)->getSingleAtomic();
|
||||
}
|
||||
} elseif ($c instanceof EnumNameFetch) {
|
||||
return Type::getString($c->case)->getSingleAtomic();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TMixed;
|
||||
}
|
||||
|
||||
|
||||
@@ -825,8 +825,6 @@ class Methods
|
||||
return null;
|
||||
}
|
||||
|
||||
$candidate_type = null;
|
||||
|
||||
foreach ($class_storage->overridden_method_ids[$appearing_method_name] as $overridden_method_id) {
|
||||
$overridden_storage = $this->getStorage($overridden_method_id);
|
||||
|
||||
|
||||
@@ -920,8 +920,8 @@ class Populator
|
||||
$fq_class_name = $storage->name;
|
||||
$fq_class_name_lc = strtolower($fq_class_name);
|
||||
|
||||
if ($parent_storage->sealed_methods) {
|
||||
$storage->sealed_methods = true;
|
||||
if ($parent_storage->sealed_methods !== null) {
|
||||
$storage->sealed_methods = $parent_storage->sealed_methods;
|
||||
}
|
||||
|
||||
// register where they appear (can never be in a trait)
|
||||
@@ -1032,8 +1032,8 @@ class Populator
|
||||
ClassLikeStorage $storage,
|
||||
ClassLikeStorage $parent_storage
|
||||
): void {
|
||||
if ($parent_storage->sealed_properties) {
|
||||
$storage->sealed_properties = true;
|
||||
if ($parent_storage->sealed_properties !== null) {
|
||||
$storage->sealed_properties = $parent_storage->sealed_properties;
|
||||
}
|
||||
|
||||
// register where they appear (can never be in a trait)
|
||||
|
||||
@@ -291,6 +291,7 @@ class Scanner
|
||||
private function shouldScan(string $file_path): bool
|
||||
{
|
||||
return $this->file_provider->fileExists($file_path)
|
||||
&& !$this->file_provider->isDirectory($file_path)
|
||||
&& (!isset($this->scanned_files[$file_path])
|
||||
|| (isset($this->files_to_deep_scan[$file_path]) && !$this->scanned_files[$file_path]));
|
||||
}
|
||||
|
||||
87
vendor/vimeo/psalm/src/Psalm/Internal/Codebase/StorageByPatternResolver.php
vendored
Normal file
87
vendor/vimeo/psalm/src/Psalm/Internal/Codebase/StorageByPatternResolver.php
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\Codebase;
|
||||
|
||||
use Psalm\Storage\ClassConstantStorage;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\EnumCaseStorage;
|
||||
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class StorageByPatternResolver
|
||||
{
|
||||
public const RESOLVE_CONSTANTS = 1;
|
||||
public const RESOLVE_ENUMS = 2;
|
||||
|
||||
/**
|
||||
* @return array<string,ClassConstantStorage>
|
||||
*/
|
||||
public function resolveConstants(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $pattern
|
||||
): array {
|
||||
$constants = $class_like_storage->constants;
|
||||
|
||||
if (strpos($pattern, '*') === false) {
|
||||
if (isset($constants[$pattern])) {
|
||||
return [$pattern => $constants[$pattern]];
|
||||
}
|
||||
|
||||
return [];
|
||||
} elseif ($pattern === '*') {
|
||||
return $constants;
|
||||
}
|
||||
|
||||
$regex_pattern = sprintf('#^%s$#', str_replace('*', '.*?', $pattern));
|
||||
$matched_constants = [];
|
||||
|
||||
foreach ($constants as $constant => $class_constant_storage) {
|
||||
if (preg_match($regex_pattern, $constant) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched_constants[$constant] = $class_constant_storage;
|
||||
}
|
||||
|
||||
return $matched_constants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,EnumCaseStorage>
|
||||
*/
|
||||
public function resolveEnums(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $pattern
|
||||
): array {
|
||||
$enum_cases = $class_like_storage->enum_cases;
|
||||
if (strpos($pattern, '*') === false) {
|
||||
if (isset($enum_cases[$pattern])) {
|
||||
return [$pattern => $enum_cases[$pattern]];
|
||||
}
|
||||
|
||||
return [];
|
||||
} elseif ($pattern === '*') {
|
||||
return $enum_cases;
|
||||
}
|
||||
|
||||
$regex_pattern = sprintf('#^%s$#', str_replace('*', '.*?', $pattern));
|
||||
$matched_enums = [];
|
||||
foreach ($enum_cases as $enum_case_name => $enum_case_storage) {
|
||||
if (preg_match($regex_pattern, $enum_case_name) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched_enums[$enum_case_name] = $enum_case_storage;
|
||||
}
|
||||
|
||||
return $matched_enums;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use Psalm\Plugin\EventHandler\AfterFunctionLikeAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\AfterMethodCallAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\AfterStatementAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\BeforeAddIssueInterface;
|
||||
use Psalm\Plugin\EventHandler\BeforeExpressionAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\BeforeFileAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\BeforeStatementAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
|
||||
@@ -32,6 +33,7 @@ use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\BeforeExpressionAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\BeforeFileAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\StringInterpreterEvent;
|
||||
@@ -77,6 +79,13 @@ class EventDispatcher
|
||||
*/
|
||||
public array $after_every_function_checks = [];
|
||||
|
||||
/**
|
||||
* Static methods to be called before expression checks are completed
|
||||
*
|
||||
* @var list<class-string<BeforeExpressionAnalysisInterface>>
|
||||
*/
|
||||
public array $before_expression_checks = [];
|
||||
|
||||
/**
|
||||
* Static methods to be called after expression checks have completed
|
||||
*
|
||||
@@ -197,6 +206,10 @@ class EventDispatcher
|
||||
$this->after_every_function_checks[] = $class;
|
||||
}
|
||||
|
||||
if (is_subclass_of($class, BeforeExpressionAnalysisInterface::class)) {
|
||||
$this->before_expression_checks[] = $class;
|
||||
}
|
||||
|
||||
if (is_subclass_of($class, AfterExpressionAnalysisInterface::class)) {
|
||||
$this->after_expression_checks[] = $class;
|
||||
}
|
||||
@@ -284,6 +297,17 @@ class EventDispatcher
|
||||
}
|
||||
}
|
||||
|
||||
public function dispatchBeforeExpressionAnalysis(BeforeExpressionAnalysisEvent $event): ?bool
|
||||
{
|
||||
foreach ($this->before_expression_checks as $handler) {
|
||||
if ($handler::beforeExpressionAnalysis($event) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function dispatchAfterExpressionAnalysis(AfterExpressionAnalysisEvent $event): ?bool
|
||||
{
|
||||
foreach ($this->after_expression_checks as $handler) {
|
||||
|
||||
@@ -137,7 +137,7 @@ class FunctionDocblockManipulator
|
||||
if ($param->type) {
|
||||
$this->param_typehint_offsets[$param->var->name] = [
|
||||
(int) $param->type->getAttribute('startFilePos'),
|
||||
(int) $param->type->getAttribute('endFilePos'),
|
||||
(int) $param->type->getAttribute('endFilePos') + 1,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class PsalmRestarter extends XdebugHandler
|
||||
'jit_buffer_size' => 512 * 1024 * 1024,
|
||||
'optimization_level' => '0x7FFEBFFF',
|
||||
'preload' => '',
|
||||
'log_verbosity_level' => 0,
|
||||
];
|
||||
|
||||
private bool $required = false;
|
||||
@@ -70,6 +71,7 @@ class PsalmRestarter extends XdebugHandler
|
||||
$opcache_settings = [
|
||||
'enable_cli' => in_array(ini_get('opcache.enable_cli'), ['1', 'true', true, 1]),
|
||||
'jit' => (int) ini_get('opcache.jit'),
|
||||
'log_verbosity_level' => (int) ini_get('opcache.log_verbosity_level'),
|
||||
'optimization_level' => (string) ini_get('opcache.optimization_level'),
|
||||
'preload' => (string) ini_get('opcache.preload'),
|
||||
'jit_buffer_size' => self::toBytes(ini_get('opcache.jit_buffer_size')),
|
||||
@@ -146,6 +148,7 @@ class PsalmRestarter extends XdebugHandler
|
||||
'-dopcache.jit=1205',
|
||||
'-dopcache.optimization_level=0x7FFEBFFF',
|
||||
'-dopcache.preload=',
|
||||
'-dopcache.log_verbosity_level=0',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Client;
|
||||
|
||||
use Amp\Promise;
|
||||
use Generator;
|
||||
use JsonMapper;
|
||||
use LanguageServerProtocol\Diagnostic;
|
||||
use LanguageServerProtocol\TextDocumentIdentifier;
|
||||
use LanguageServerProtocol\TextDocumentItem;
|
||||
use Psalm\Internal\LanguageServer\ClientHandler;
|
||||
|
||||
use function Amp\call;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
@@ -23,12 +17,12 @@ class TextDocument
|
||||
{
|
||||
private ClientHandler $handler;
|
||||
|
||||
private JsonMapper $mapper;
|
||||
private LanguageServer $server;
|
||||
|
||||
public function __construct(ClientHandler $handler, JsonMapper $mapper)
|
||||
public function __construct(ClientHandler $handler, LanguageServer $server)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
$this->mapper = $mapper;
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,40 +30,18 @@ class TextDocument
|
||||
*
|
||||
* @param Diagnostic[] $diagnostics
|
||||
*/
|
||||
public function publishDiagnostics(string $uri, array $diagnostics): void
|
||||
public function publishDiagnostics(string $uri, array $diagnostics, ?int $version = null): void
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideDiagnostics) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->server->logDebug("textDocument/publishDiagnostics");
|
||||
|
||||
$this->handler->notify('textDocument/publishDiagnostics', [
|
||||
'uri' => $uri,
|
||||
'diagnostics' => $diagnostics,
|
||||
'version' => $version,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The content request is sent from a server to a client
|
||||
* to request the current content of a text document identified by the URI
|
||||
*
|
||||
* @param TextDocumentIdentifier $textDocument The document to get the content for
|
||||
* @return Promise<TextDocumentItem> The document's current content
|
||||
* @psalm-suppress MixedReturnTypeCoercion due to Psalm bug
|
||||
*/
|
||||
public function xcontent(TextDocumentIdentifier $textDocument): Promise
|
||||
{
|
||||
return call(
|
||||
/**
|
||||
* @return Generator<int, Promise<object>, object, TextDocumentItem>
|
||||
*/
|
||||
function () use ($textDocument) {
|
||||
/** @var Promise<object> */
|
||||
$promise = $this->handler->request(
|
||||
'textDocument/xcontent',
|
||||
['textDocument' => $textDocument],
|
||||
);
|
||||
|
||||
$result = yield $promise;
|
||||
|
||||
/** @var TextDocumentItem */
|
||||
return $this->mapper->map($result, new TextDocumentItem);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
59
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Client/Workspace.php
vendored
Normal file
59
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Client/Workspace.php
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Client;
|
||||
|
||||
use Amp\Promise;
|
||||
use JsonMapper;
|
||||
use Psalm\Internal\LanguageServer\ClientHandler;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Workspace
|
||||
{
|
||||
private ClientHandler $handler;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedProperty
|
||||
*/
|
||||
private JsonMapper $mapper;
|
||||
|
||||
private LanguageServer $server;
|
||||
|
||||
public function __construct(ClientHandler $handler, JsonMapper $mapper, LanguageServer $server)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
$this->mapper = $mapper;
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* The workspace/configuration request is sent from the server to the client to
|
||||
* fetch configuration settings from the client. The request can fetch several
|
||||
* configuration settings in one roundtrip. The order of the returned configuration
|
||||
* settings correspond to the order of the passed ConfigurationItems (e.g. the first
|
||||
* item in the response is the result for the first configuration item in the params).
|
||||
*
|
||||
* @param string $section The configuration section asked for.
|
||||
* @param string|null $scopeUri The scope to get the configuration section for.
|
||||
*/
|
||||
public function requestConfiguration(string $section, ?string $scopeUri = null): Promise
|
||||
{
|
||||
$this->server->logDebug("workspace/configuration");
|
||||
|
||||
/** @var Promise<object> */
|
||||
return $this->handler->request('workspace/configuration', [
|
||||
'items' => [
|
||||
[
|
||||
'section' => $section,
|
||||
'scopeUri' => $scopeUri,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
129
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/ClientConfiguration.php
vendored
Normal file
129
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/ClientConfiguration.php
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use LanguageServerProtocol\MessageType;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ClientConfiguration
|
||||
{
|
||||
|
||||
/**
|
||||
* Location of Baseline file
|
||||
*/
|
||||
public ?string $baseline = null;
|
||||
|
||||
/**
|
||||
* TCP Server Address
|
||||
*/
|
||||
public ?string $TCPServerAddress = null;
|
||||
|
||||
/**
|
||||
* Use TCP in server mode (default is client)
|
||||
*/
|
||||
public ?bool $TCPServerMode = null;
|
||||
|
||||
/**
|
||||
* Hide Warnings or not
|
||||
*/
|
||||
public ?bool $hideWarnings = null;
|
||||
|
||||
/**
|
||||
* Provide Completion or not
|
||||
*/
|
||||
public ?bool $provideCompletion = null;
|
||||
|
||||
/**
|
||||
* Provide GoTo Definitions or not
|
||||
*/
|
||||
public ?bool $provideDefinition = null;
|
||||
|
||||
/**
|
||||
* Provide Hover Requests or not
|
||||
*/
|
||||
public ?bool $provideHover = null;
|
||||
|
||||
/**
|
||||
* Provide Signature Help or not
|
||||
*/
|
||||
public ?bool $provideSignatureHelp = null;
|
||||
|
||||
/**
|
||||
* Provide Code Actions or not
|
||||
*/
|
||||
public ?bool $provideCodeActions = null;
|
||||
|
||||
/**
|
||||
* Provide Diagnostics or not
|
||||
*/
|
||||
public ?bool $provideDiagnostics = null;
|
||||
|
||||
/**
|
||||
* Provide Completion or not
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedProperty
|
||||
*/
|
||||
public ?bool $findUnusedVariables = null;
|
||||
|
||||
/**
|
||||
* Look for dead code
|
||||
*
|
||||
* @var 'always'|'auto'|null
|
||||
*/
|
||||
public ?string $findUnusedCode = null;
|
||||
|
||||
/**
|
||||
* Log Level
|
||||
*
|
||||
* @see MessageType
|
||||
*/
|
||||
public ?int $logLevel = null;
|
||||
|
||||
/**
|
||||
* If added, the language server will not respond to onChange events.
|
||||
* You can also specify a line count over which Psalm will not run on-change events.
|
||||
*/
|
||||
public ?int $onchangeLineLimit = null;
|
||||
|
||||
/**
|
||||
* Debounce time in milliseconds for onChange events
|
||||
*/
|
||||
public ?int $onChangeDebounceMs = null;
|
||||
|
||||
/**
|
||||
* Undocumented function
|
||||
*
|
||||
* @param 'always'|'auto'|null $findUnusedCode
|
||||
*/
|
||||
public function __construct(
|
||||
bool $hideWarnings = true,
|
||||
?bool $provideCompletion = null,
|
||||
?bool $provideDefinition = null,
|
||||
?bool $provideHover = null,
|
||||
?bool $provideSignatureHelp = null,
|
||||
?bool $provideCodeActions = null,
|
||||
?bool $provideDiagnostics = null,
|
||||
?bool $findUnusedVariables = null,
|
||||
?string $findUnusedCode = null,
|
||||
?int $logLevel = null,
|
||||
?int $onchangeLineLimit = null,
|
||||
?string $baseline = null
|
||||
) {
|
||||
$this->hideWarnings = $hideWarnings;
|
||||
$this->provideCompletion = $provideCompletion;
|
||||
$this->provideDefinition = $provideDefinition;
|
||||
$this->provideHover = $provideHover;
|
||||
$this->provideSignatureHelp = $provideSignatureHelp;
|
||||
$this->provideCodeActions = $provideCodeActions;
|
||||
$this->provideDiagnostics = $provideDiagnostics;
|
||||
$this->findUnusedVariables = $findUnusedVariables;
|
||||
$this->findUnusedCode = $findUnusedCode;
|
||||
$this->logLevel = $logLevel;
|
||||
$this->onchangeLineLimit = $onchangeLineLimit;
|
||||
$this->baseline = $baseline;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ use Amp\Promise;
|
||||
use Generator;
|
||||
|
||||
use function Amp\call;
|
||||
use function error_log;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -59,7 +58,6 @@ class ClientHandler
|
||||
|
||||
$listener =
|
||||
function (Message $msg) use ($id, $deferred, &$listener): void {
|
||||
error_log('request handler');
|
||||
/**
|
||||
* @psalm-suppress UndefinedPropertyFetch
|
||||
* @psalm-suppress MixedArgument
|
||||
|
||||
@@ -5,7 +5,14 @@ declare(strict_types=1);
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use JsonMapper;
|
||||
use LanguageServerProtocol\LogMessage;
|
||||
use LanguageServerProtocol\LogTrace;
|
||||
use Psalm\Internal\LanguageServer\Client\TextDocument as ClientTextDocument;
|
||||
use Psalm\Internal\LanguageServer\Client\Workspace as ClientWorkspace;
|
||||
|
||||
use function is_null;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -17,44 +24,159 @@ class LanguageClient
|
||||
*/
|
||||
public ClientTextDocument $textDocument;
|
||||
|
||||
/**
|
||||
* Handles workspace/* methods
|
||||
*/
|
||||
public ClientWorkspace $workspace;
|
||||
|
||||
/**
|
||||
* The client handler
|
||||
*/
|
||||
private ClientHandler $handler;
|
||||
|
||||
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
||||
{
|
||||
$this->handler = new ClientHandler($reader, $writer);
|
||||
$mapper = new JsonMapper;
|
||||
/**
|
||||
* The Language Server
|
||||
*/
|
||||
private LanguageServer $server;
|
||||
|
||||
$this->textDocument = new ClientTextDocument($this->handler, $mapper);
|
||||
/**
|
||||
* The Client Configuration
|
||||
*/
|
||||
public ClientConfiguration $clientConfiguration;
|
||||
|
||||
public function __construct(
|
||||
ProtocolReader $reader,
|
||||
ProtocolWriter $writer,
|
||||
LanguageServer $server,
|
||||
ClientConfiguration $clientConfiguration
|
||||
) {
|
||||
$this->handler = new ClientHandler($reader, $writer);
|
||||
$this->server = $server;
|
||||
|
||||
$this->textDocument = new ClientTextDocument($this->handler, $this->server);
|
||||
$this->workspace = new ClientWorkspace($this->handler, new JsonMapper, $this->server);
|
||||
$this->clientConfiguration = $clientConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request Configuration from Client and save it
|
||||
*/
|
||||
public function refreshConfiguration(): void
|
||||
{
|
||||
$capabilities = $this->server->clientCapabilities;
|
||||
if ($capabilities && $capabilities->workspace && $capabilities->workspace->configuration) {
|
||||
$this->workspace->requestConfiguration('psalm')->onResolve(function ($error, $value): void {
|
||||
if ($error) {
|
||||
$this->server->logError('There was an error getting configuration');
|
||||
} else {
|
||||
/** @var array<int, object> $value */
|
||||
[$config] = $value;
|
||||
$this->configurationRefreshed((array) $config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A notification to log the trace of the server’s execution.
|
||||
* The amount and content of these notifications depends on the current trace configuration.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function logTrace(LogTrace $logTrace): void
|
||||
{
|
||||
//If trace is 'off', the server should not send any logTrace notification.
|
||||
if (is_null($this->server->trace) || $this->server->trace === 'off') {
|
||||
return;
|
||||
}
|
||||
|
||||
//If trace is 'messages', the server should not add the 'verbose' field in the LogTraceParams.
|
||||
if ($this->server->trace === 'messages') {
|
||||
$logTrace->verbose = null;
|
||||
}
|
||||
|
||||
$this->handler->notify(
|
||||
'$/logTrace',
|
||||
$logTrace,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a log message to the client.
|
||||
*
|
||||
* @param string $message The message to send to the client.
|
||||
* @psalm-param 1|2|3|4 $type
|
||||
* @param int $type The log type:
|
||||
* - 1 = Error
|
||||
* - 2 = Warning
|
||||
* - 3 = Info
|
||||
* - 4 = Log
|
||||
*/
|
||||
public function logMessage(string $message, int $type = 4, string $method = 'window/logMessage'): void
|
||||
public function logMessage(LogMessage $logMessage): void
|
||||
{
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
|
||||
|
||||
if ($type < 1 || $type > 4) {
|
||||
$type = 4;
|
||||
}
|
||||
|
||||
$this->handler->notify(
|
||||
$method,
|
||||
[
|
||||
'type' => $type,
|
||||
'message' => $message,
|
||||
],
|
||||
'window/logMessage',
|
||||
$logMessage,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The telemetry notification is sent from the
|
||||
* server to the client to ask the client to log
|
||||
* a telemetry event.
|
||||
*
|
||||
* The protocol doesn’t specify the payload since no
|
||||
* interpretation of the data happens in the protocol.
|
||||
* Most clients even don’t handle the event directly
|
||||
* but forward them to the extensions owing the corresponding
|
||||
* server issuing the event.
|
||||
*/
|
||||
public function event(LogMessage $logMessage): void
|
||||
{
|
||||
$this->handler->notify(
|
||||
'telemetry/event',
|
||||
$logMessage,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration Refreshed from Client
|
||||
*
|
||||
* @param array $config
|
||||
*/
|
||||
private function configurationRefreshed(array $config): void
|
||||
{
|
||||
//do things when the config is refreshed
|
||||
|
||||
if (empty($config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array */
|
||||
$array = json_decode(json_encode($config), true);
|
||||
|
||||
if (isset($array['hideWarnings'])) {
|
||||
$this->clientConfiguration->hideWarnings = (bool) $array['hideWarnings'];
|
||||
}
|
||||
|
||||
if (isset($array['provideCompletion'])) {
|
||||
$this->clientConfiguration->provideCompletion = (bool) $array['provideCompletion'];
|
||||
}
|
||||
|
||||
if (isset($array['provideDefinition'])) {
|
||||
$this->clientConfiguration->provideDefinition = (bool) $array['provideDefinition'];
|
||||
}
|
||||
|
||||
if (isset($array['provideHover'])) {
|
||||
$this->clientConfiguration->provideHover = (bool) $array['provideHover'];
|
||||
}
|
||||
|
||||
if (isset($array['provideSignatureHelp'])) {
|
||||
$this->clientConfiguration->provideSignatureHelp = (bool) $array['provideSignatureHelp'];
|
||||
}
|
||||
|
||||
if (isset($array['provideCodeActions'])) {
|
||||
$this->clientConfiguration->provideCodeActions = (bool) $array['provideCodeActions'];
|
||||
}
|
||||
|
||||
if (isset($array['provideDiagnostics'])) {
|
||||
$this->clientConfiguration->provideDiagnostics = (bool) $array['provideDiagnostics'];
|
||||
}
|
||||
|
||||
if (isset($array['findUnusedVariables'])) {
|
||||
$this->clientConfiguration->findUnusedVariables = (bool) $array['findUnusedVariables'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,6 @@ class Message
|
||||
|
||||
/**
|
||||
* Parses a message
|
||||
*
|
||||
* @psalm-suppress UnusedMethod
|
||||
*/
|
||||
public static function parse(string $msg): Message
|
||||
{
|
||||
@@ -35,7 +33,9 @@ class Message
|
||||
foreach ($parts as $line) {
|
||||
if ($line) {
|
||||
$pair = explode(': ', $line);
|
||||
$obj->headers[$pair[0]] = $pair[1];
|
||||
if (isset($pair[1])) {
|
||||
$obj->headers[$pair[0]] = $pair[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ class Message
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
|
||||
$body = (string)$this->body;
|
||||
$contentLength = strlen($body);
|
||||
$this->headers['Content-Length'] = (string) $contentLength;
|
||||
|
||||
58
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php
vendored
Normal file
58
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use JsonSerializable;
|
||||
use LanguageServerProtocol\MarkupContent;
|
||||
use LanguageServerProtocol\MarkupKind;
|
||||
use ReturnTypeWillChange;
|
||||
|
||||
use function get_object_vars;
|
||||
|
||||
/**
|
||||
* @psalm-api
|
||||
* @internal
|
||||
*/
|
||||
class PHPMarkdownContent extends MarkupContent implements JsonSerializable
|
||||
{
|
||||
public string $code;
|
||||
|
||||
public ?string $title = null;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public function __construct(string $code, ?string $title = null, ?string $description = null)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->title = $title;
|
||||
$this->description = $description;
|
||||
|
||||
$markdown = '';
|
||||
if ($title !== null) {
|
||||
$markdown = "**$title**\n\n";
|
||||
}
|
||||
if ($description !== null) {
|
||||
$markdown = "$markdown$description\n\n";
|
||||
}
|
||||
parent::__construct(
|
||||
MarkupKind::MARKDOWN,
|
||||
"$markdown```php\n<?php\n$code\n```",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is needed because VSCode Does not like nulls
|
||||
* meaning if a null is sent then this will not compute
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$vars = get_object_vars($this);
|
||||
unset($vars['title'], $vars['description'], $vars['code']);
|
||||
return $vars;
|
||||
}
|
||||
}
|
||||
35
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Progress.php
vendored
Normal file
35
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Progress.php
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use Psalm\Progress\Progress as Base;
|
||||
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Progress extends Base
|
||||
{
|
||||
|
||||
private ?LanguageServer $server = null;
|
||||
|
||||
public function setServer(LanguageServer $server): void
|
||||
{
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
public function debug(string $message): void
|
||||
{
|
||||
if ($this->server) {
|
||||
$this->server->logDebug(str_replace("\n", "", $message));
|
||||
}
|
||||
}
|
||||
|
||||
public function write(string $message): void
|
||||
{
|
||||
if ($this->server) {
|
||||
$this->server->logInfo(str_replace("\n", "", $message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,9 @@ class ProtocolStreamReader implements ProtocolReader
|
||||
$this->buffer = '';
|
||||
} elseif (substr($this->buffer, -2) === "\r\n") {
|
||||
$parts = explode(':', $this->buffer);
|
||||
$this->headers[$parts[0]] = trim($parts[1]);
|
||||
if (isset($parts[1])) {
|
||||
$this->headers[$parts[0]] = trim($parts[1]);
|
||||
}
|
||||
$this->buffer = '';
|
||||
}
|
||||
break;
|
||||
|
||||
47
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php
vendored
Normal file
47
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Provider;
|
||||
|
||||
use Psalm\Internal\Provider\ClassLikeStorageCacheProvider as InternalClassLikeStorageCacheProvider;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ClassLikeStorageCacheProvider extends InternalClassLikeStorageCacheProvider
|
||||
{
|
||||
/** @var array<string, ClassLikeStorage> */
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function writeToCache(ClassLikeStorage $storage, ?string $file_path, ?string $file_contents): void
|
||||
{
|
||||
$fq_classlike_name_lc = strtolower($storage->name);
|
||||
$this->cache[$fq_classlike_name_lc] = $storage;
|
||||
}
|
||||
|
||||
public function getLatestFromCache(
|
||||
string $fq_classlike_name_lc,
|
||||
?string $file_path,
|
||||
?string $file_contents
|
||||
): ClassLikeStorage {
|
||||
$cached_value = $this->loadFromCache($fq_classlike_name_lc);
|
||||
|
||||
if (!$cached_value) {
|
||||
throw new UnexpectedValueException('Should be in cache');
|
||||
}
|
||||
|
||||
return $cached_value;
|
||||
}
|
||||
|
||||
private function loadFromCache(string $fq_classlike_name_lc): ?ClassLikeStorage
|
||||
{
|
||||
return $this->cache[$fq_classlike_name_lc] ?? null;
|
||||
}
|
||||
}
|
||||
280
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php
vendored
Normal file
280
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Provider;
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Internal\Provider\FileReferenceCacheProvider as InternalFileReferenceCacheProvider;
|
||||
|
||||
/**
|
||||
* Used to determine which files reference other files, necessary for using the --diff
|
||||
* option from the command line.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FileReferenceCacheProvider extends InternalFileReferenceCacheProvider
|
||||
{
|
||||
private ?array $cached_file_references = null;
|
||||
|
||||
private ?array $cached_classlike_files = null;
|
||||
|
||||
private ?array $cached_method_class_references = null;
|
||||
|
||||
private ?array $cached_nonmethod_class_references = null;
|
||||
|
||||
private ?array $cached_method_member_references = null;
|
||||
|
||||
private ?array $cached_method_dependencies = null;
|
||||
|
||||
private ?array $cached_method_property_references = null;
|
||||
|
||||
private ?array $cached_method_method_return_references = null;
|
||||
|
||||
private ?array $cached_file_member_references = null;
|
||||
|
||||
private ?array $cached_file_property_references = null;
|
||||
|
||||
private ?array $cached_file_method_return_references = null;
|
||||
|
||||
private ?array $cached_method_missing_member_references = null;
|
||||
|
||||
private ?array $cached_file_missing_member_references = null;
|
||||
|
||||
private ?array $cached_unknown_member_references = null;
|
||||
|
||||
private ?array $cached_method_param_uses = null;
|
||||
|
||||
private ?array $cached_issues = null;
|
||||
|
||||
/** @var array<string, array<string, int>> */
|
||||
private array $cached_correct_methods = [];
|
||||
|
||||
/**
|
||||
* @var array<
|
||||
* string,
|
||||
* array{
|
||||
* 0: array<int, array{0: int, 1: non-empty-string}>,
|
||||
* 1: array<int, array{0: int, 1: non-empty-string}>,
|
||||
* 2: array<int, array{0: int, 1: non-empty-string, 2: int}>
|
||||
* }
|
||||
* >
|
||||
*/
|
||||
private array $cached_file_maps = [];
|
||||
|
||||
public function __construct(Config $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getCachedFileReferences(): ?array
|
||||
{
|
||||
return $this->cached_file_references;
|
||||
}
|
||||
|
||||
public function getCachedClassLikeFiles(): ?array
|
||||
{
|
||||
return $this->cached_classlike_files;
|
||||
}
|
||||
|
||||
public function getCachedMethodClassReferences(): ?array
|
||||
{
|
||||
return $this->cached_method_class_references;
|
||||
}
|
||||
|
||||
public function getCachedNonMethodClassReferences(): ?array
|
||||
{
|
||||
return $this->cached_nonmethod_class_references;
|
||||
}
|
||||
|
||||
public function getCachedFileMemberReferences(): ?array
|
||||
{
|
||||
return $this->cached_file_member_references;
|
||||
}
|
||||
|
||||
public function getCachedFilePropertyReferences(): ?array
|
||||
{
|
||||
return $this->cached_file_property_references;
|
||||
}
|
||||
|
||||
public function getCachedFileMethodReturnReferences(): ?array
|
||||
{
|
||||
return $this->cached_file_method_return_references;
|
||||
}
|
||||
|
||||
public function getCachedMethodMemberReferences(): ?array
|
||||
{
|
||||
return $this->cached_method_member_references;
|
||||
}
|
||||
|
||||
public function getCachedMethodDependencies(): ?array
|
||||
{
|
||||
return $this->cached_method_dependencies;
|
||||
}
|
||||
|
||||
public function getCachedMethodPropertyReferences(): ?array
|
||||
{
|
||||
return $this->cached_method_property_references;
|
||||
}
|
||||
|
||||
public function getCachedMethodMethodReturnReferences(): ?array
|
||||
{
|
||||
return $this->cached_method_method_return_references;
|
||||
}
|
||||
|
||||
public function getCachedFileMissingMemberReferences(): ?array
|
||||
{
|
||||
return $this->cached_file_missing_member_references;
|
||||
}
|
||||
|
||||
public function getCachedMixedMemberNameReferences(): ?array
|
||||
{
|
||||
return $this->cached_unknown_member_references;
|
||||
}
|
||||
|
||||
public function getCachedMethodMissingMemberReferences(): ?array
|
||||
{
|
||||
return $this->cached_method_missing_member_references;
|
||||
}
|
||||
|
||||
public function getCachedMethodParamUses(): ?array
|
||||
{
|
||||
return $this->cached_method_param_uses;
|
||||
}
|
||||
|
||||
public function getCachedIssues(): ?array
|
||||
{
|
||||
return $this->cached_issues;
|
||||
}
|
||||
|
||||
public function setCachedFileReferences(array $file_references): void
|
||||
{
|
||||
$this->cached_file_references = $file_references;
|
||||
}
|
||||
|
||||
public function setCachedClassLikeFiles(array $file_references): void
|
||||
{
|
||||
$this->cached_classlike_files = $file_references;
|
||||
}
|
||||
|
||||
public function setCachedMethodClassReferences(array $method_class_references): void
|
||||
{
|
||||
$this->cached_method_class_references = $method_class_references;
|
||||
}
|
||||
|
||||
public function setCachedNonMethodClassReferences(array $file_class_references): void
|
||||
{
|
||||
$this->cached_nonmethod_class_references = $file_class_references;
|
||||
}
|
||||
|
||||
public function setCachedMethodMemberReferences(array $member_references): void
|
||||
{
|
||||
$this->cached_method_member_references = $member_references;
|
||||
}
|
||||
|
||||
public function setCachedMethodDependencies(array $member_references): void
|
||||
{
|
||||
$this->cached_method_dependencies = $member_references;
|
||||
}
|
||||
|
||||
public function setCachedMethodPropertyReferences(array $property_references): void
|
||||
{
|
||||
$this->cached_method_property_references = $property_references;
|
||||
}
|
||||
|
||||
public function setCachedMethodMethodReturnReferences(array $method_return_references): void
|
||||
{
|
||||
$this->cached_method_method_return_references = $method_return_references;
|
||||
}
|
||||
|
||||
public function setCachedMethodMissingMemberReferences(array $member_references): void
|
||||
{
|
||||
$this->cached_method_missing_member_references = $member_references;
|
||||
}
|
||||
|
||||
public function setCachedFileMemberReferences(array $member_references): void
|
||||
{
|
||||
$this->cached_file_member_references = $member_references;
|
||||
}
|
||||
|
||||
public function setCachedFilePropertyReferences(array $property_references): void
|
||||
{
|
||||
$this->cached_file_property_references = $property_references;
|
||||
}
|
||||
|
||||
public function setCachedFileMethodReturnReferences(array $method_return_references): void
|
||||
{
|
||||
$this->cached_file_method_return_references = $method_return_references;
|
||||
}
|
||||
|
||||
public function setCachedFileMissingMemberReferences(array $member_references): void
|
||||
{
|
||||
$this->cached_file_missing_member_references = $member_references;
|
||||
}
|
||||
|
||||
public function setCachedMixedMemberNameReferences(array $references): void
|
||||
{
|
||||
$this->cached_unknown_member_references = $references;
|
||||
}
|
||||
|
||||
public function setCachedMethodParamUses(array $uses): void
|
||||
{
|
||||
$this->cached_method_param_uses = $uses;
|
||||
}
|
||||
|
||||
public function setCachedIssues(array $issues): void
|
||||
{
|
||||
$this->cached_issues = $issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, int>>
|
||||
*/
|
||||
public function getAnalyzedMethodCache(): array
|
||||
{
|
||||
return $this->cached_correct_methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, int>> $analyzed_methods
|
||||
*/
|
||||
public function setAnalyzedMethodCache(array $analyzed_methods): void
|
||||
{
|
||||
$this->cached_correct_methods = $analyzed_methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<
|
||||
* string,
|
||||
* array{
|
||||
* 0: array<int, array{0: int, 1: non-empty-string}>,
|
||||
* 1: array<int, array{0: int, 1: non-empty-string}>,
|
||||
* 2: array<int, array{0: int, 1: non-empty-string, 2: int}>
|
||||
* }
|
||||
* >
|
||||
*/
|
||||
public function getFileMapCache(): array
|
||||
{
|
||||
return $this->cached_file_maps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<
|
||||
* string,
|
||||
* array{
|
||||
* 0: array<int, array{0: int, 1: non-empty-string}>,
|
||||
* 1: array<int, array{0: int, 1: non-empty-string}>,
|
||||
* 2: array<int, array{0: int, 1: non-empty-string, 2: int}>
|
||||
* }
|
||||
* > $file_maps
|
||||
*/
|
||||
public function setFileMapCache(array $file_maps): void
|
||||
{
|
||||
$this->cached_file_maps = $file_maps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{int, int}> $mixed_counts
|
||||
*/
|
||||
public function setTypeCoverage(array $mixed_counts): void
|
||||
{
|
||||
}
|
||||
}
|
||||
48
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php
vendored
Normal file
48
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Provider;
|
||||
|
||||
use Psalm\Internal\Provider\FileStorageCacheProvider as InternalFileStorageCacheProvider;
|
||||
use Psalm\Storage\FileStorage;
|
||||
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class FileStorageCacheProvider extends InternalFileStorageCacheProvider
|
||||
{
|
||||
/** @var array<lowercase-string, FileStorage> */
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function writeToCache(FileStorage $storage, string $file_contents): void
|
||||
{
|
||||
$file_path = strtolower($storage->file_path);
|
||||
$this->cache[$file_path] = $storage;
|
||||
}
|
||||
|
||||
public function getLatestFromCache(string $file_path, string $file_contents): ?FileStorage
|
||||
{
|
||||
$cached_value = $this->loadFromCache(strtolower($file_path));
|
||||
|
||||
if (!$cached_value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $cached_value;
|
||||
}
|
||||
|
||||
public function removeCacheForFile(string $file_path): void
|
||||
{
|
||||
unset($this->cache[strtolower($file_path)]);
|
||||
}
|
||||
|
||||
private function loadFromCache(string $file_path): ?FileStorage
|
||||
{
|
||||
return $this->cache[strtolower($file_path)] ?? null;
|
||||
}
|
||||
}
|
||||
109
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php
vendored
Normal file
109
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Provider;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Internal\Provider\ParserCacheProvider as InternalParserCacheProvider;
|
||||
|
||||
use function microtime;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ParserCacheProvider extends InternalParserCacheProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $file_contents_cache = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $file_content_hash = [];
|
||||
|
||||
/**
|
||||
* @var array<string, list<PhpParser\Node\Stmt>>
|
||||
*/
|
||||
private array $statements_cache = [];
|
||||
|
||||
/**
|
||||
* @var array<string, float>
|
||||
*/
|
||||
private array $statements_cache_time = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function loadStatementsFromCache(
|
||||
string $file_path,
|
||||
int $file_modified_time,
|
||||
string $file_content_hash
|
||||
): ?array {
|
||||
if (isset($this->statements_cache[$file_path])
|
||||
&& $this->statements_cache_time[$file_path] >= $file_modified_time
|
||||
&& $this->file_content_hash[$file_path] === $file_content_hash
|
||||
) {
|
||||
return $this->statements_cache[$file_path];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<PhpParser\Node\Stmt>|null
|
||||
*/
|
||||
public function loadExistingStatementsFromCache(string $file_path): ?array
|
||||
{
|
||||
if (isset($this->statements_cache[$file_path])) {
|
||||
return $this->statements_cache[$file_path];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<PhpParser\Node\Stmt> $stmts
|
||||
*/
|
||||
public function saveStatementsToCache(
|
||||
string $file_path,
|
||||
string $file_content_hash,
|
||||
array $stmts,
|
||||
bool $touch_only
|
||||
): void {
|
||||
$this->statements_cache[$file_path] = $stmts;
|
||||
$this->statements_cache_time[$file_path] = microtime(true);
|
||||
$this->file_content_hash[$file_path] = $file_content_hash;
|
||||
}
|
||||
|
||||
public function loadExistingFileContentsFromCache(string $file_path): ?string
|
||||
{
|
||||
if (isset($this->file_contents_cache[$file_path])) {
|
||||
return $this->file_contents_cache[$file_path];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function cacheFileContents(string $file_path, string $file_contents): void
|
||||
{
|
||||
$this->file_contents_cache[$file_path] = $file_contents;
|
||||
}
|
||||
|
||||
public function deleteOldParserCaches(float $time_before): int
|
||||
{
|
||||
$this->existing_file_content_hashes = null;
|
||||
$this->new_file_content_hashes = [];
|
||||
|
||||
$this->file_contents_cache = [];
|
||||
$this->file_content_hash = [];
|
||||
$this->statements_cache = [];
|
||||
$this->statements_cache_time = [];
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function saveFileContentHashes(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
41
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php
vendored
Normal file
41
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Provider;
|
||||
|
||||
use Psalm\Internal\Provider\ProjectCacheProvider as PsalmProjectCacheProvider;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ProjectCacheProvider extends PsalmProjectCacheProvider
|
||||
{
|
||||
private int $last_run = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getLastRun(string $psalm_version): int
|
||||
{
|
||||
return $this->last_run;
|
||||
}
|
||||
|
||||
public function processSuccessfulRun(float $start_time, string $psalm_version): void
|
||||
{
|
||||
$this->last_run = (int) $start_time;
|
||||
}
|
||||
|
||||
public function canDiffFiles(): bool
|
||||
{
|
||||
return $this->last_run > 0;
|
||||
}
|
||||
|
||||
public function hasLockfileChanged(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updateComposerLockHash(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
22
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Reference.php
vendored
Normal file
22
vendor/vimeo/psalm/src/Psalm/Internal/LanguageServer/Reference.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use LanguageServerProtocol\Range;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Reference
|
||||
{
|
||||
public string $file_path;
|
||||
public string $symbol;
|
||||
public Range $range;
|
||||
|
||||
public function __construct(string $file_path, string $symbol, Range $range)
|
||||
{
|
||||
$this->file_path = $file_path;
|
||||
$this->symbol = $symbol;
|
||||
$this->range = $range;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,12 @@ namespace Psalm\Internal\LanguageServer\Server;
|
||||
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use LanguageServerProtocol\CodeAction;
|
||||
use LanguageServerProtocol\CodeActionContext;
|
||||
use LanguageServerProtocol\CodeActionKind;
|
||||
use LanguageServerProtocol\CompletionList;
|
||||
use LanguageServerProtocol\Hover;
|
||||
use LanguageServerProtocol\Location;
|
||||
use LanguageServerProtocol\MarkupContent;
|
||||
use LanguageServerProtocol\MarkupKind;
|
||||
use LanguageServerProtocol\Position;
|
||||
use LanguageServerProtocol\Range;
|
||||
use LanguageServerProtocol\SignatureHelp;
|
||||
@@ -21,6 +22,7 @@ use LanguageServerProtocol\TextEdit;
|
||||
use LanguageServerProtocol\VersionedTextDocumentIdentifier;
|
||||
use LanguageServerProtocol\WorkspaceEdit;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Exception\TypeParseTreeException;
|
||||
use Psalm\Exception\UnanalyzedFileException;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer;
|
||||
@@ -28,7 +30,6 @@ use UnexpectedValueException;
|
||||
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function error_log;
|
||||
use function preg_match;
|
||||
use function substr_count;
|
||||
|
||||
@@ -68,36 +69,41 @@ class TextDocument
|
||||
*/
|
||||
public function didOpen(TextDocumentItem $textDocument): void
|
||||
{
|
||||
$this->server->logDebug(
|
||||
'textDocument/didOpen',
|
||||
['version' => $textDocument->version, 'uri' => $textDocument->uri],
|
||||
);
|
||||
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->codebase->removeTemporaryFileChanges($file_path);
|
||||
$this->codebase->file_provider->openFile($file_path);
|
||||
$this->codebase->file_provider->setOpenContents($file_path, $textDocument->text);
|
||||
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
$this->server->queueOpenFileAnalysis($file_path, $textDocument->uri, $textDocument->version);
|
||||
}
|
||||
|
||||
/**
|
||||
* The document save notification is sent from the client to the server when the document was saved in the client
|
||||
*
|
||||
* @param TextDocumentItem $textDocument the document that was opened
|
||||
* @param ?string $text the content when saved
|
||||
* @param TextDocumentIdentifier $textDocument the document that was opened
|
||||
* @param string|null $text Optional the content when saved. Depends on the includeText value
|
||||
* when the save notification was requested.
|
||||
*/
|
||||
public function didSave(TextDocumentItem $textDocument, ?string $text): void
|
||||
public function didSave(TextDocumentIdentifier $textDocument, ?string $text = null): void
|
||||
{
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
$this->server->logDebug(
|
||||
'textDocument/didSave',
|
||||
['uri' => (array) $textDocument],
|
||||
);
|
||||
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return;
|
||||
}
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
// reopen file
|
||||
$this->codebase->removeTemporaryFileChanges($file_path);
|
||||
$this->codebase->file_provider->setOpenContents($file_path, (string) $text);
|
||||
$this->codebase->file_provider->setOpenContents($file_path, $text);
|
||||
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
$this->server->queueSaveFileAnalysis($file_path, $textDocument->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,13 +114,14 @@ class TextDocument
|
||||
*/
|
||||
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges): void
|
||||
{
|
||||
$this->server->logDebug(
|
||||
'textDocument/didChange',
|
||||
['version' => $textDocument->version, 'uri' => $textDocument->uri],
|
||||
);
|
||||
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($contentChanges) === 1 && $contentChanges[0]->range === null) {
|
||||
if (count($contentChanges) === 1 && isset($contentChanges[0]) && $contentChanges[0]->range === null) {
|
||||
$new_content = $contentChanges[0]->text;
|
||||
} else {
|
||||
throw new UnexpectedValueException('Not expecting partial diff');
|
||||
@@ -126,8 +133,8 @@ class TextDocument
|
||||
}
|
||||
}
|
||||
|
||||
$this->codebase->addTemporaryFileChanges($file_path, $new_content);
|
||||
$this->server->queueTemporaryFileAnalysis($file_path, $textDocument->uri);
|
||||
$this->codebase->addTemporaryFileChanges($file_path, $new_content, $textDocument->version);
|
||||
$this->server->queueChangeFileAnalysis($file_path, $textDocument->uri, $textDocument->version);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,6 +149,11 @@ class TextDocument
|
||||
*/
|
||||
public function didClose(TextDocumentIdentifier $textDocument): void
|
||||
{
|
||||
$this->server->logDebug(
|
||||
'textDocument/didClose',
|
||||
['uri' => $textDocument->uri],
|
||||
);
|
||||
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
$this->codebase->file_provider->closeFile($file_path);
|
||||
@@ -158,24 +170,34 @@ class TextDocument
|
||||
*/
|
||||
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideDefinition) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
'textDocument/definition',
|
||||
);
|
||||
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
try {
|
||||
$reference_location = $this->codebase->getReferenceAtPosition($file_path, $position);
|
||||
$reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->codebase->file_provider->openFile($file_path);
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
if ($reference_location === null) {
|
||||
if ($reference === null) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
[$reference] = $reference_location;
|
||||
|
||||
$code_location = $this->codebase->getSymbolLocation($file_path, $reference);
|
||||
$code_location = $this->codebase->getSymbolLocationByReference($reference);
|
||||
|
||||
if (!$code_location) {
|
||||
return new Success(null);
|
||||
@@ -202,39 +224,44 @@ class TextDocument
|
||||
*/
|
||||
public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
{
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
try {
|
||||
$reference_location = $this->codebase->getReferenceAtPosition($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->codebase->file_provider->openFile($file_path);
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
|
||||
if (!$this->server->client->clientConfiguration->provideHover) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
if ($reference_location === null) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
[$reference, $range] = $reference_location;
|
||||
|
||||
$symbol_information = $this->codebase->getSymbolInformation($file_path, $reference);
|
||||
|
||||
if ($symbol_information === null) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$content = "```php\n" . $symbol_information['type'] . "\n```";
|
||||
if (isset($symbol_information['description'])) {
|
||||
$content .= "\n---\n" . $symbol_information['description'];
|
||||
}
|
||||
$contents = new MarkupContent(
|
||||
MarkupKind::MARKDOWN,
|
||||
$content,
|
||||
$this->server->logDebug(
|
||||
'textDocument/hover',
|
||||
);
|
||||
|
||||
return new Success(new Hover($contents, $range));
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
try {
|
||||
$reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
if ($reference === null) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
try {
|
||||
$markup = $this->codebase->getMarkupContentForSymbolByReference($reference);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
if ($markup === null) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
return new Success(new Hover($markup, $reference->range));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,56 +276,74 @@ class TextDocument
|
||||
*
|
||||
* @param TextDocumentIdentifier $textDocument The text document
|
||||
* @param Position $position The position
|
||||
* @psalm-return Promise<array<never, never>>|Promise<CompletionList>
|
||||
* @psalm-return Promise<array<empty, empty>>|Promise<CompletionList>|Promise<null>
|
||||
*/
|
||||
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideCompletion) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
'textDocument/completion',
|
||||
);
|
||||
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success([]);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
try {
|
||||
$completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->codebase->file_provider->openFile($file_path);
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
if ($completion_data) {
|
||||
[$recent_type, $gap, $offset] = $completion_data;
|
||||
|
||||
return new Success([]);
|
||||
if ($gap === '->' || $gap === '::') {
|
||||
$snippetSupport = ($this->server->clientCapabilities &&
|
||||
$this->server->clientCapabilities->textDocument &&
|
||||
$this->server->clientCapabilities->textDocument->completion &&
|
||||
$this->server->clientCapabilities->textDocument->completion->completionItem &&
|
||||
$this->server->clientCapabilities->textDocument->completion->completionItem->snippetSupport)
|
||||
? true : false;
|
||||
$completion_items =
|
||||
$this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport);
|
||||
} elseif ($gap === '[') {
|
||||
$completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type);
|
||||
} else {
|
||||
$completion_items = $this->codebase->getCompletionItemsForPartialSymbol(
|
||||
$recent_type,
|
||||
$offset,
|
||||
$file_path,
|
||||
);
|
||||
}
|
||||
return new Success(new CompletionList($completion_items, false));
|
||||
}
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
try {
|
||||
$type_context = $this->codebase->getTypeContextAtPosition($file_path, $position);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
error_log('completion errored at ' . $position->line . ':' . $position->character.
|
||||
', Reason: '.$e->getMessage());
|
||||
return new Success([]);
|
||||
}
|
||||
|
||||
if (!$completion_data && !$type_context) {
|
||||
error_log('completion not found at ' . $position->line . ':' . $position->character);
|
||||
return new Success([]);
|
||||
}
|
||||
|
||||
if ($completion_data) {
|
||||
[$recent_type, $gap, $offset] = $completion_data;
|
||||
|
||||
if ($gap === '->' || $gap === '::') {
|
||||
$completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap);
|
||||
} elseif ($gap === '[') {
|
||||
$completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type);
|
||||
} else {
|
||||
$completion_items = $this->codebase->getCompletionItemsForPartialSymbol(
|
||||
$recent_type,
|
||||
$offset,
|
||||
$file_path,
|
||||
);
|
||||
if ($type_context) {
|
||||
$completion_items = $this->codebase->getCompletionItemsForType($type_context);
|
||||
return new Success(new CompletionList($completion_items, false));
|
||||
}
|
||||
} else {
|
||||
$completion_items = $this->codebase->getCompletionItemsForType($type_context);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
return new Success(new CompletionList($completion_items, false));
|
||||
$this->server->logError('completion not found at ' . $position->line . ':' . $position->character);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,96 +352,134 @@ class TextDocument
|
||||
*/
|
||||
public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideSignatureHelp) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
'textDocument/signatureHelp',
|
||||
);
|
||||
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
try {
|
||||
$argument_location = $this->codebase->getFunctionArgumentAtPosition($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->codebase->file_provider->openFile($file_path);
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
|
||||
return new Success(new SignatureHelp());
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
if ($argument_location === null) {
|
||||
return new Success(new SignatureHelp());
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path);
|
||||
try {
|
||||
$signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
if (!$signature_information) {
|
||||
return new Success(new SignatureHelp());
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
return new Success(new SignatureHelp([
|
||||
$signature_information,
|
||||
], 0, $argument_location[1]));
|
||||
return new Success(
|
||||
new SignatureHelp(
|
||||
[$signature_information],
|
||||
0,
|
||||
$argument_location[1],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The code action request is sent from the client to the server to compute commands
|
||||
* for a given text document and range. These commands are typically code fixes to
|
||||
* either fix problems or to beautify/refactor code.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedParam
|
||||
*/
|
||||
public function codeAction(TextDocumentIdentifier $textDocument, Range $range): Promise
|
||||
public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideCodeActions) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
'textDocument/codeAction',
|
||||
);
|
||||
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
if (!$this->codebase->file_provider->isOpen($file_path)) {
|
||||
|
||||
//Don't report code actions for files we arent watching
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$issues = $this->server->getCurrentIssues();
|
||||
|
||||
if (empty($issues[$file_path])) {
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
$file_contents = $this->codebase->getFileContents($file_path);
|
||||
|
||||
$offsetStart = $range->start->toOffset($file_contents);
|
||||
$offsetEnd = $range->end->toOffset($file_contents);
|
||||
|
||||
$fixers = [];
|
||||
foreach ($issues[$file_path] as $issue) {
|
||||
if ($offsetStart === $issue->from && $offsetEnd === $issue->to) {
|
||||
$snippetRange = new Range(
|
||||
new Position($issue->line_from-1),
|
||||
new Position($issue->line_to),
|
||||
);
|
||||
foreach ($context->diagnostics as $diagnostic) {
|
||||
if ($diagnostic->source !== 'psalm') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indentation = '';
|
||||
if (preg_match('/^(\s*)/', $issue->snippet, $matches)) {
|
||||
$indentation = $matches[1] ?? '';
|
||||
}
|
||||
/** @var array{type: string, snippet: string, line_from: int, line_to: int} */
|
||||
$data = (array)$diagnostic->data;
|
||||
|
||||
/**
|
||||
* Suppress Psalm because ther are bugs in how
|
||||
* LanguageServer's signature of WorkspaceEdit is declared:
|
||||
*
|
||||
* See:
|
||||
* https://github.com/felixfbecker/php-language-server-protocol
|
||||
* See:
|
||||
* https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceEdit
|
||||
*/
|
||||
$edit = new WorkspaceEdit([
|
||||
//$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
//$contents = $this->codebase->file_provider->getContents($file_path);
|
||||
|
||||
$snippetRange = new Range(
|
||||
new Position($data['line_from']-1),
|
||||
new Position($data['line_to']),
|
||||
);
|
||||
|
||||
$indentation = '';
|
||||
if (preg_match('/^(\s*)/', $data['snippet'], $matches)) {
|
||||
$indentation = $matches[1] ?? '';
|
||||
}
|
||||
|
||||
//Suppress Ability
|
||||
$fixers["suppress.{$data['type']}"] = new CodeAction(
|
||||
"Suppress {$data['type']} for this line",
|
||||
CodeActionKind::QUICK_FIX,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new WorkspaceEdit([
|
||||
$textDocument->uri => [
|
||||
new TextEdit(
|
||||
$snippetRange,
|
||||
"{$indentation}/**\n".
|
||||
"{$indentation} * @psalm-suppress {$issue->type}\n".
|
||||
"{$indentation} */\n".
|
||||
"{$issue->snippet}\n",
|
||||
"{$indentation}/** @psalm-suppress {$data['type']} */\n".
|
||||
"{$data['snippet']}\n",
|
||||
),
|
||||
],
|
||||
]);
|
||||
]),
|
||||
);
|
||||
|
||||
//Suppress Ability
|
||||
$fixers["suppress.{$issue->type}"] = [
|
||||
'title' => "Suppress {$issue->type} for this line",
|
||||
'kind' => 'quickfix',
|
||||
'edit' => $edit,
|
||||
];
|
||||
}
|
||||
/*
|
||||
$fixers["fixAll.{$diagnostic->data->type}"] = new CodeAction(
|
||||
"FixAll {$diagnostic->data->type} for this file",
|
||||
CodeActionKind::QUICK_FIX,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new Command(
|
||||
"Fix All",
|
||||
"psalm.fixall",
|
||||
[
|
||||
'uri' => $textDocument->uri,
|
||||
'type' => $diagnostic->data->type
|
||||
]
|
||||
)
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
if (empty($fixers)) {
|
||||
|
||||
@@ -4,11 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Server;
|
||||
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use InvalidArgumentException;
|
||||
use LanguageServerProtocol\FileChangeType;
|
||||
use LanguageServerProtocol\FileEvent;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Internal\Composer;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer;
|
||||
use Psalm\Internal\Provider\FileReferenceProvider;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function in_array;
|
||||
use function realpath;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all workspace/* methods
|
||||
@@ -46,9 +56,35 @@ class Workspace
|
||||
*/
|
||||
public function didChangeWatchedFiles(array $changes): void
|
||||
{
|
||||
$this->server->logDebug(
|
||||
'workspace/didChangeWatchedFiles',
|
||||
);
|
||||
|
||||
$realFiles = array_filter(
|
||||
array_map(function (FileEvent $change) {
|
||||
try {
|
||||
return LanguageServer::uriToPath($change->uri);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}, $changes),
|
||||
);
|
||||
|
||||
$composerLockFile = realpath(Composer::getLockFilePath($this->codebase->config->base_dir));
|
||||
if (in_array($composerLockFile, $realFiles)) {
|
||||
$this->server->logInfo('Composer.lock file changed. Reloading codebase');
|
||||
FileReferenceProvider::clearCache();
|
||||
$this->server->queueFileAnalysisWithOpenedFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($changes as $change) {
|
||||
$file_path = LanguageServer::uriToPath($change->uri);
|
||||
|
||||
if ($composerLockFile === $file_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($change->type === FileChangeType::DELETED) {
|
||||
$this->codebase->invalidateInformationForFile($file_path);
|
||||
continue;
|
||||
@@ -62,10 +98,64 @@ class Workspace
|
||||
continue;
|
||||
}
|
||||
|
||||
//If the file is currently open then dont analyse it because its tracked by the client
|
||||
//If the file is currently open then dont analize it because its tracked in didChange
|
||||
if (!$this->codebase->file_provider->isOpen($file_path)) {
|
||||
$this->server->queueFileAnalysis($file_path, $change->uri);
|
||||
$this->server->queueClosedFileAnalysis($file_path, $change->uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A notification sent from the client to the server to signal the change of configuration settings.
|
||||
*
|
||||
* @param mixed $settings
|
||||
* @psalm-suppress PossiblyUnusedMethod, PossiblyUnusedParam
|
||||
*/
|
||||
public function didChangeConfiguration($settings): void
|
||||
{
|
||||
$this->server->logDebug(
|
||||
'workspace/didChangeConfiguration',
|
||||
);
|
||||
$this->server->client->refreshConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* The workspace/executeCommand request is sent from the client to the server to
|
||||
* trigger command execution on the server.
|
||||
*
|
||||
* @param mixed $arguments
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function executeCommand(string $command, $arguments): Promise
|
||||
{
|
||||
$this->server->logDebug(
|
||||
'workspace/executeCommand',
|
||||
[
|
||||
'command' => $command,
|
||||
'arguments' => $arguments,
|
||||
],
|
||||
);
|
||||
|
||||
switch ($command) {
|
||||
case 'psalm.analyze.uri':
|
||||
/** @var array{uri: string} */
|
||||
$arguments = (array) $arguments;
|
||||
$file = LanguageServer::uriToPath($arguments['uri']);
|
||||
$this->codebase->reloadFiles(
|
||||
$this->project_analyzer,
|
||||
[$file],
|
||||
true,
|
||||
);
|
||||
|
||||
$this->codebase->analyzer->addFilesToAnalyze(
|
||||
[$file => $file],
|
||||
);
|
||||
$this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false);
|
||||
|
||||
$this->server->emitVersionedIssues([$file => $arguments['uri']]);
|
||||
break;
|
||||
}
|
||||
|
||||
return new Success(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,10 +241,24 @@ class ClassLikeDocblockParser
|
||||
if (isset($parsed_docblock->tags['psalm-seal-properties'])) {
|
||||
$info->sealed_properties = true;
|
||||
}
|
||||
if (isset($parsed_docblock->tags['psalm-no-seal-properties'])) {
|
||||
$info->sealed_properties = false;
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-seal-methods'])) {
|
||||
$info->sealed_methods = true;
|
||||
}
|
||||
if (isset($parsed_docblock->tags['psalm-no-seal-methods'])) {
|
||||
$info->sealed_methods = false;
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-inheritors'])) {
|
||||
foreach ($parsed_docblock->tags['psalm-inheritors'] as $template_line) {
|
||||
$doc_line_parts = CommentAnalyzer::splitDocLine($template_line);
|
||||
$doc_line_parts[0] = CommentAnalyzer::sanitizeDocblockType($doc_line_parts[0]);
|
||||
$info->inheritors = $doc_line_parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-immutable'])
|
||||
|| isset($parsed_docblock->tags['psalm-mutation-free'])
|
||||
@@ -296,6 +310,9 @@ class ClassLikeDocblockParser
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->combined_tags['method'])) {
|
||||
if ($info->sealed_methods === null) {
|
||||
$info->sealed_methods = true;
|
||||
}
|
||||
foreach ($parsed_docblock->combined_tags['method'] as $offset => $method_entry) {
|
||||
$method_entry = preg_replace('/[ \t]+/', ' ', trim($method_entry));
|
||||
|
||||
@@ -481,6 +498,13 @@ class ClassLikeDocblockParser
|
||||
|
||||
$info->public_api = isset($parsed_docblock->tags['psalm-api']) || isset($parsed_docblock->tags['api']);
|
||||
|
||||
if (isset($parsed_docblock->tags['property'])
|
||||
&& $codebase->config->docblock_property_types_seal_properties
|
||||
&& $info->sealed_properties === null
|
||||
) {
|
||||
$info->sealed_properties = true;
|
||||
}
|
||||
|
||||
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property');
|
||||
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'psalm-property');
|
||||
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property-read');
|
||||
|
||||
@@ -143,6 +143,7 @@ class ClassLikeNodeScanner
|
||||
|
||||
/**
|
||||
* @return false|null
|
||||
* @psalm-suppress ComplexMethod
|
||||
*/
|
||||
public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
|
||||
{
|
||||
@@ -420,10 +421,19 @@ class ClassLikeNodeScanner
|
||||
|
||||
if ($template_map[1] !== null && $template_map[2] !== null) {
|
||||
if (trim($template_map[2])) {
|
||||
$type_string = $template_map[2];
|
||||
try {
|
||||
$type_string = CommentAnalyzer::splitDocLine($type_string)[0];
|
||||
} catch (DocblockParseException $e) {
|
||||
throw new DocblockParseException(
|
||||
$type_string . ' is not a valid type: ' . $e->getMessage(),
|
||||
);
|
||||
}
|
||||
$type_string = CommentAnalyzer::sanitizeDocblockType($type_string);
|
||||
try {
|
||||
$template_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$template_map[2],
|
||||
$type_string,
|
||||
$this->aliases,
|
||||
$storage->template_types,
|
||||
$this->type_aliases,
|
||||
@@ -529,6 +539,31 @@ class ClassLikeNodeScanner
|
||||
$storage->sealed_properties = $docblock_info->sealed_properties;
|
||||
$storage->sealed_methods = $docblock_info->sealed_methods;
|
||||
|
||||
|
||||
if ($docblock_info->inheritors) {
|
||||
try {
|
||||
$storage->inheritors = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$docblock_info->inheritors,
|
||||
$storage->aliases,
|
||||
$storage->template_types ?? [],
|
||||
$storage->type_aliases,
|
||||
$fq_classlike_name,
|
||||
),
|
||||
null,
|
||||
$storage->template_types ?? [],
|
||||
$storage->type_aliases,
|
||||
true,
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
'@psalm-inheritors contains invalid reference:' . $e->getMessage(),
|
||||
$name_location ?? $class_location,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($docblock_info->properties) {
|
||||
foreach ($docblock_info->properties as $property) {
|
||||
$pseudo_property_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
|
||||
@@ -567,8 +602,6 @@ class ClassLikeNodeScanner
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$storage->sealed_properties = true;
|
||||
}
|
||||
|
||||
foreach ($docblock_info->methods as $method) {
|
||||
@@ -595,8 +628,6 @@ class ClassLikeNodeScanner
|
||||
$lc_method_name,
|
||||
);
|
||||
}
|
||||
|
||||
$storage->sealed_methods = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1173,7 +1204,7 @@ class ClassLikeNodeScanner
|
||||
$storage->template_type_uses_count[$generic_class_lc] = count($atomic_type->type_params);
|
||||
|
||||
foreach ($atomic_type->type_params as $type_param) {
|
||||
$used_type_parameters[] = $type_param;
|
||||
$used_type_parameters[] = $type_param->replaceClassLike('self', $storage->name);
|
||||
}
|
||||
|
||||
$storage->template_extended_offsets[$atomic_type->value] = $used_type_parameters;
|
||||
@@ -1905,9 +1936,12 @@ class ClassLikeNodeScanner
|
||||
}
|
||||
|
||||
$type_string = str_replace("\n", '', implode('', $var_line_parts));
|
||||
|
||||
// Strip any remaining characters after the last grouping character >, } or )
|
||||
$type_string = preg_replace('/(?<=[>})])[^>})]*$/', '', $type_string, 1);
|
||||
try {
|
||||
$type_string = CommentAnalyzer::splitDocLine($type_string)[0];
|
||||
} catch (DocblockParseException $e) {
|
||||
throw new DocblockParseException($type_string . ' is not a valid type: '.$e->getMessage());
|
||||
}
|
||||
$type_string = CommentAnalyzer::sanitizeDocblockType($type_string);
|
||||
|
||||
try {
|
||||
$type_tokens = TypeTokenizer::getFullyQualifiedTokens(
|
||||
|
||||
@@ -15,6 +15,8 @@ use Psalm\Internal\Scanner\UnresolvedConstant\ArraySpread;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\ArrayValue;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\ClassConstant;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\Constant;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\EnumNameFetch;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\EnumValueFetch;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\KeyValuePair;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\ScalarValue;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstant\UnresolvedAdditionOp;
|
||||
@@ -34,6 +36,7 @@ use function assert;
|
||||
use function class_exists;
|
||||
use function function_exists;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function interface_exists;
|
||||
use function strtolower;
|
||||
|
||||
@@ -297,6 +300,24 @@ class ExpressionResolver
|
||||
return new ArrayValue($items);
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\PropertyFetch
|
||||
&& $stmt->var instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||
&& $stmt->var->class instanceof PhpParser\Node\Name
|
||||
&& $stmt->var->name instanceof PhpParser\Node\Identifier
|
||||
&& $stmt->name instanceof PhpParser\Node\Identifier
|
||||
&& in_array($stmt->name->name, ['name', 'value', true])
|
||||
) {
|
||||
$enum_fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
|
||||
$stmt->var->class,
|
||||
$aliases,
|
||||
);
|
||||
if ($stmt->name->name === 'value') {
|
||||
return new EnumValueFetch($enum_fq_class_name, $stmt->var->name->name);
|
||||
} elseif ($stmt->name->name === 'name') {
|
||||
return new EnumNameFetch($enum_fq_class_name, $stmt->var->name->name);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ use Psalm\CodeLocation;
|
||||
use Psalm\CodeLocation\DocblockTypeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Config;
|
||||
use Psalm\Exception\DocblockParseException;
|
||||
use Psalm\Exception\InvalidMethodOverrideException;
|
||||
use Psalm\Exception\TypeParseTreeException;
|
||||
use Psalm\Internal\Analyzer\CommentAnalyzer;
|
||||
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
|
||||
use Psalm\Internal\Scanner\FileScanner;
|
||||
use Psalm\Internal\Scanner\FunctionDocblockComment;
|
||||
@@ -61,6 +63,7 @@ use function strlen;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function substr_replace;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
@@ -1252,7 +1255,7 @@ class FunctionLikeDocblockScanner
|
||||
|
||||
if (strpos($assertion['param_name'], $param->name.'->') === 0) {
|
||||
$storage->assertions[] = new Possibilities(
|
||||
str_replace($param->name, (string) $i, $assertion['param_name']),
|
||||
substr_replace($assertion['param_name'], (string) $i, 0, strlen($param->name)),
|
||||
$assertion_type_parts,
|
||||
);
|
||||
continue 2;
|
||||
@@ -1439,10 +1442,17 @@ class FunctionLikeDocblockScanner
|
||||
|
||||
if ($template_map[1] !== null && $template_map[2] !== null) {
|
||||
if (trim($template_map[2])) {
|
||||
$type_string = $template_map[2];
|
||||
try {
|
||||
$type_string = CommentAnalyzer::splitDocLine($type_string)[0];
|
||||
} catch (DocblockParseException $e) {
|
||||
throw new DocblockParseException($type_string . ' is not a valid type: '.$e->getMessage());
|
||||
}
|
||||
$type_string = CommentAnalyzer::sanitizeDocblockType($type_string);
|
||||
try {
|
||||
$template_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$template_map[2],
|
||||
$type_string,
|
||||
$aliases,
|
||||
$storage->template_types + ($template_types ?: []),
|
||||
$type_aliases,
|
||||
|
||||
@@ -238,6 +238,12 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements FileSour
|
||||
$var_id = '$' . $var->name;
|
||||
|
||||
$functionlike_node_scanner->storage->global_variables[$var_id] = true;
|
||||
|
||||
if (isset($this->codebase->config->globals[$var_id])) {
|
||||
$var_type = Type::parseString($this->codebase->config->globals[$var_id]);
|
||||
/** @psalm-suppress UnusedMethodCall */
|
||||
$var_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +49,14 @@ class ClassLikeStorageProvider
|
||||
return self::$storage[$fq_classlike_name_lc];
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function has(string $fq_classlike_name): bool
|
||||
{
|
||||
$fq_classlike_name_lc = strtolower($fq_classlike_name);
|
||||
|
||||
/** @psalm-suppress ImpureStaticProperty Used only for caching */
|
||||
return isset(self::$storage[$fq_classlike_name_lc]);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,16 +20,26 @@ class FakeFileProvider extends FileProvider
|
||||
*/
|
||||
public array $fake_file_times = [];
|
||||
|
||||
/**
|
||||
* @var array<string, true>
|
||||
*/
|
||||
public array $fake_directories = [];
|
||||
|
||||
public function fileExists(string $file_path): bool
|
||||
{
|
||||
return isset($this->fake_files[$file_path]) || parent::fileExists($file_path);
|
||||
}
|
||||
|
||||
public function isDirectory(string $file_path): bool
|
||||
{
|
||||
return isset($this->fake_directories[$file_path]) || parent::isDirectory($file_path);
|
||||
}
|
||||
|
||||
/** @psalm-external-mutation-free */
|
||||
public function getContents(string $file_path, bool $go_to_source = false): string
|
||||
{
|
||||
if (!$go_to_source && isset($this->temp_files[$file_path])) {
|
||||
return $this->temp_files[$file_path];
|
||||
return $this->temp_files[$file_path]['content'];
|
||||
}
|
||||
|
||||
return $this->fake_files[$file_path] ?? parent::getContents($file_path);
|
||||
@@ -40,10 +50,10 @@ class FakeFileProvider extends FileProvider
|
||||
$this->fake_files[$file_path] = $file_contents;
|
||||
}
|
||||
|
||||
public function setOpenContents(string $file_path, string $file_contents): void
|
||||
public function setOpenContents(string $file_path, ?string $file_contents = null): void
|
||||
{
|
||||
if (isset($this->fake_files[$file_path])) {
|
||||
$this->fake_files[$file_path] = $file_contents;
|
||||
$this->fake_files[$file_path] = $file_contents ?? $this->getContents($file_path, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use const DIRECTORY_SEPARATOR;
|
||||
class FileProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
* @var array<string, array{version: ?int, content: string}>
|
||||
*/
|
||||
protected array $temp_files = [];
|
||||
|
||||
@@ -33,32 +33,31 @@ class FileProvider
|
||||
*/
|
||||
protected static array $open_files = [];
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $open_files_paths = [];
|
||||
|
||||
public function getContents(string $file_path, bool $go_to_source = false): string
|
||||
{
|
||||
if (!$go_to_source && isset($this->temp_files[$file_path])) {
|
||||
return $this->temp_files[$file_path];
|
||||
return $this->temp_files[$file_path]['content'];
|
||||
}
|
||||
|
||||
/** @psalm-suppress ImpureStaticProperty Used only for caching */
|
||||
if (isset(self::$open_files[$file_path])) {
|
||||
return self::$open_files[$file_path];
|
||||
}
|
||||
|
||||
/** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */
|
||||
if (!file_exists($file_path)) {
|
||||
throw new UnexpectedValueException('File ' . $file_path . ' should exist to get contents');
|
||||
}
|
||||
|
||||
/** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */
|
||||
if (is_dir($file_path)) {
|
||||
throw new UnexpectedValueException('File ' . $file_path . ' is a directory');
|
||||
}
|
||||
|
||||
/** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */
|
||||
$file_contents = (string) file_get_contents($file_path);
|
||||
|
||||
/** @psalm-suppress ImpureStaticProperty Used only for caching */
|
||||
self::$open_files[$file_path] = $file_contents;
|
||||
|
||||
return $file_contents;
|
||||
@@ -71,16 +70,19 @@ class FileProvider
|
||||
}
|
||||
|
||||
if (isset($this->temp_files[$file_path])) {
|
||||
$this->temp_files[$file_path] = $file_contents;
|
||||
$this->temp_files[$file_path] = [
|
||||
'version'=> null,
|
||||
'content' => $file_contents,
|
||||
];
|
||||
}
|
||||
|
||||
file_put_contents($file_path, $file_contents);
|
||||
}
|
||||
|
||||
public function setOpenContents(string $file_path, string $file_contents): void
|
||||
public function setOpenContents(string $file_path, ?string $file_contents = null): void
|
||||
{
|
||||
if (isset(self::$open_files[$file_path])) {
|
||||
self::$open_files[$file_path] = $file_contents;
|
||||
self::$open_files[$file_path] = $file_contents ?? $this->getContents($file_path, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +95,19 @@ class FileProvider
|
||||
return (int) filemtime($file_path);
|
||||
}
|
||||
|
||||
public function addTemporaryFileChanges(string $file_path, string $new_content): void
|
||||
public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void
|
||||
{
|
||||
$this->temp_files[$file_path] = $new_content;
|
||||
if (isset($this->temp_files[$file_path]) &&
|
||||
$version !== null &&
|
||||
$this->temp_files[$file_path]['version'] !== null &&
|
||||
$version < $this->temp_files[$file_path]['version']
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$this->temp_files[$file_path] = [
|
||||
'version' => $version,
|
||||
'content' => $new_content,
|
||||
];
|
||||
}
|
||||
|
||||
public function removeTemporaryFileChanges(string $file_path): void
|
||||
@@ -103,9 +115,15 @@ class FileProvider
|
||||
unset($this->temp_files[$file_path]);
|
||||
}
|
||||
|
||||
public function getOpenFilesPath(): array
|
||||
{
|
||||
return $this->open_files_paths;
|
||||
}
|
||||
|
||||
public function openFile(string $file_path): void
|
||||
{
|
||||
self::$open_files[$file_path] = $this->getContents($file_path, true);
|
||||
$this->open_files_paths[$file_path] = $file_path;
|
||||
}
|
||||
|
||||
public function isOpen(string $file_path): bool
|
||||
@@ -115,7 +133,11 @@ class FileProvider
|
||||
|
||||
public function closeFile(string $file_path): void
|
||||
{
|
||||
unset($this->temp_files[$file_path], self::$open_files[$file_path]);
|
||||
unset(
|
||||
$this->temp_files[$file_path],
|
||||
self::$open_files[$file_path],
|
||||
$this->open_files_paths[$file_path],
|
||||
);
|
||||
}
|
||||
|
||||
public function fileExists(string $file_path): bool
|
||||
@@ -123,6 +145,11 @@ class FileProvider
|
||||
return file_exists($file_path);
|
||||
}
|
||||
|
||||
public function isDirectory(string $file_path): bool
|
||||
{
|
||||
return is_dir($file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $file_extensions
|
||||
* @param null|callable(string):bool $filter
|
||||
|
||||
@@ -22,8 +22,8 @@ use Psalm\Internal\Provider\ReturnTypeProvider\ArrayReduceReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayReverseReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\ArraySliceReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\ArraySpliceReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayUniqueReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\BasenameReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\DateReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\DirnameReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider;
|
||||
@@ -36,6 +36,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\MbInternalEncodingReturnTypeProvi
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\MinMaxReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\MktimeReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\ParseUrlReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\PowReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\RandReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\RoundReturnTypeProvider;
|
||||
use Psalm\Internal\Provider\ReturnTypeProvider\StrReplaceReturnTypeProvider;
|
||||
@@ -81,7 +82,6 @@ class FunctionReturnTypeProvider
|
||||
$this->registerClass(ArraySliceReturnTypeProvider::class);
|
||||
$this->registerClass(ArraySpliceReturnTypeProvider::class);
|
||||
$this->registerClass(ArrayReverseReturnTypeProvider::class);
|
||||
$this->registerClass(ArrayUniqueReturnTypeProvider::class);
|
||||
$this->registerClass(ArrayFillReturnTypeProvider::class);
|
||||
$this->registerClass(ArrayFillKeysReturnTypeProvider::class);
|
||||
$this->registerClass(FilterVarReturnTypeProvider::class);
|
||||
@@ -103,6 +103,8 @@ class FunctionReturnTypeProvider
|
||||
$this->registerClass(InArrayReturnTypeProvider::class);
|
||||
$this->registerClass(RoundReturnTypeProvider::class);
|
||||
$this->registerClass(MbInternalEncodingReturnTypeProvider::class);
|
||||
$this->registerClass(DateReturnTypeProvider::class);
|
||||
$this->registerClass(PowReturnTypeProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Provider\ReturnTypeProvider;
|
||||
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
|
||||
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TNonEmptyArray;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ArrayUniqueReturnTypeProvider implements FunctionReturnTypeProviderInterface
|
||||
{
|
||||
/**
|
||||
* @return array<lowercase-string>
|
||||
*/
|
||||
public static function getFunctionIds(): array
|
||||
{
|
||||
return ['array_unique'];
|
||||
}
|
||||
|
||||
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
|
||||
{
|
||||
$statements_source = $event->getStatementsSource();
|
||||
$call_args = $event->getCallArgs();
|
||||
if (!$statements_source instanceof StatementsAnalyzer) {
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
$first_arg = $call_args[0]->value ?? null;
|
||||
|
||||
$first_arg_array = $first_arg
|
||||
&& ($first_arg_type = $statements_source->node_data->getType($first_arg))
|
||||
&& $first_arg_type->hasType('array')
|
||||
&& ($array_atomic_type = $first_arg_type->getArray())
|
||||
&& ($array_atomic_type instanceof TArray
|
||||
|| $array_atomic_type instanceof TKeyedArray)
|
||||
? $array_atomic_type
|
||||
: null;
|
||||
|
||||
if (!$first_arg_array) {
|
||||
return Type::getArray();
|
||||
}
|
||||
|
||||
if ($first_arg_array instanceof TArray) {
|
||||
if ($first_arg_array instanceof TNonEmptyArray) {
|
||||
$first_arg_array = $first_arg_array->setCount(null);
|
||||
}
|
||||
|
||||
return new Union([$first_arg_array]);
|
||||
}
|
||||
|
||||
return new Union([$first_arg_array->getGenericArrayType()]);
|
||||
}
|
||||
}
|
||||
69
vendor/vimeo/psalm/src/Psalm/Internal/Provider/ReturnTypeProvider/DateReturnTypeProvider.php
vendored
Normal file
69
vendor/vimeo/psalm/src/Psalm/Internal/Provider/ReturnTypeProvider/DateReturnTypeProvider.php
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\Provider\ReturnTypeProvider;
|
||||
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
|
||||
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_values;
|
||||
use function date;
|
||||
use function is_numeric;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class DateReturnTypeProvider implements FunctionReturnTypeProviderInterface
|
||||
{
|
||||
/**
|
||||
* @return array<lowercase-string>
|
||||
*/
|
||||
public static function getFunctionIds(): array
|
||||
{
|
||||
return ['date', 'gmdate'];
|
||||
}
|
||||
|
||||
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
|
||||
{
|
||||
$source = $event->getStatementsSource();
|
||||
if (!$source instanceof StatementsAnalyzer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$call_args = $event->getCallArgs();
|
||||
|
||||
$format_type = Type::getString();
|
||||
if (isset($call_args[0])) {
|
||||
$type = $source->node_data->getType($call_args[0]->value);
|
||||
if ($type !== null
|
||||
&& $type->isSingleStringLiteral()
|
||||
&& is_numeric(date($type->getSingleStringLiteral()->value))
|
||||
) {
|
||||
$format_type = Type::getNumericString();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($call_args[1])) {
|
||||
return $format_type;
|
||||
}
|
||||
|
||||
$type = $source->node_data->getType($call_args[1]->value);
|
||||
if ($type !== null && $type->isSingle()) {
|
||||
$atomic_type = array_values($type->getAtomicTypes())[0];
|
||||
if ($atomic_type instanceof Type\Atomic\TNumeric
|
||||
|| $atomic_type instanceof Type\Atomic\TInt
|
||||
|| $atomic_type instanceof TLiteralInt
|
||||
|| ($atomic_type instanceof TLiteralString && is_numeric($atomic_type->value))
|
||||
) {
|
||||
return $format_type;
|
||||
}
|
||||
}
|
||||
return $format_type;
|
||||
}
|
||||
}
|
||||
92
vendor/vimeo/psalm/src/Psalm/Internal/Provider/ReturnTypeProvider/PowReturnTypeProvider.php
vendored
Normal file
92
vendor/vimeo/psalm/src/Psalm/Internal/Provider/ReturnTypeProvider/PowReturnTypeProvider.php
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Provider\ReturnTypeProvider;
|
||||
|
||||
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
|
||||
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TFloat;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TLiteralFloat;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class PowReturnTypeProvider implements FunctionReturnTypeProviderInterface
|
||||
{
|
||||
/**
|
||||
* @return array<lowercase-string>
|
||||
*/
|
||||
public static function getFunctionIds(): array
|
||||
{
|
||||
return ['pow'];
|
||||
}
|
||||
|
||||
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
|
||||
{
|
||||
$call_args = $event->getCallArgs();
|
||||
|
||||
if (count($call_args) !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$first_arg = $event->getStatementsSource()->getNodeTypeProvider()->getType($call_args[0]->value);
|
||||
$second_arg = $event->getStatementsSource()->getNodeTypeProvider()->getType($call_args[1]->value);
|
||||
|
||||
$first_arg_literal = null;
|
||||
$first_arg_is_int = false;
|
||||
$first_arg_is_float = false;
|
||||
if ($first_arg !== null && $first_arg->isSingle()) {
|
||||
$first_atomic_type = $first_arg->getSingleAtomic();
|
||||
if ($first_atomic_type instanceof TInt) {
|
||||
$first_arg_is_int = true;
|
||||
} elseif ($first_atomic_type instanceof TFloat) {
|
||||
$first_arg_is_float = true;
|
||||
}
|
||||
if ($first_atomic_type instanceof TLiteralInt
|
||||
|| $first_atomic_type instanceof TLiteralFloat
|
||||
) {
|
||||
$first_arg_literal = $first_atomic_type->value;
|
||||
}
|
||||
}
|
||||
|
||||
$second_arg_literal = null;
|
||||
$second_arg_is_int = false;
|
||||
$second_arg_is_float = false;
|
||||
if ($second_arg !== null && $second_arg->isSingle()) {
|
||||
$second_atomic_type = $second_arg->getSingleAtomic();
|
||||
if ($second_atomic_type instanceof TInt) {
|
||||
$second_arg_is_int = true;
|
||||
} elseif ($second_atomic_type instanceof TFloat) {
|
||||
$second_arg_is_float = true;
|
||||
}
|
||||
if ($second_atomic_type instanceof TLiteralInt
|
||||
|| $second_atomic_type instanceof TLiteralFloat
|
||||
) {
|
||||
$second_arg_literal = $second_atomic_type->value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($first_arg_literal === 0) {
|
||||
return Type::getInt(true, 0);
|
||||
}
|
||||
if ($second_arg_literal === 0) {
|
||||
return Type::getInt(true, 1);
|
||||
}
|
||||
if ($first_arg_literal !== null && $second_arg_literal !== null) {
|
||||
return Type::getFloat($first_arg_literal ** $second_arg_literal);
|
||||
}
|
||||
if ($first_arg_is_int && $second_arg_is_int) {
|
||||
return Type::getInt();
|
||||
}
|
||||
if ($first_arg_is_float || $second_arg_is_float) {
|
||||
return Type::getFloat();
|
||||
}
|
||||
|
||||
return new Union([new TInt(), new TFloat()]);
|
||||
}
|
||||
}
|
||||
@@ -63,9 +63,9 @@ class ClassLikeDocblockComment
|
||||
*/
|
||||
public array $methods = [];
|
||||
|
||||
public bool $sealed_properties = false;
|
||||
public ?bool $sealed_properties = null;
|
||||
|
||||
public bool $sealed_methods = false;
|
||||
public ?bool $sealed_methods = null;
|
||||
|
||||
public bool $override_property_visibility = false;
|
||||
|
||||
@@ -87,6 +87,8 @@ class ClassLikeDocblockComment
|
||||
*/
|
||||
public array $imported_types = [];
|
||||
|
||||
public ?string $inheritors = null;
|
||||
|
||||
public bool $consistent_constructor = false;
|
||||
|
||||
public bool $consistent_templates = false;
|
||||
|
||||
11
vendor/vimeo/psalm/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumNameFetch.php
vendored
Normal file
11
vendor/vimeo/psalm/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumNameFetch.php
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Scanner\UnresolvedConstant;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
* @internal
|
||||
*/
|
||||
class EnumNameFetch extends EnumPropertyFetch
|
||||
{
|
||||
}
|
||||
22
vendor/vimeo/psalm/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumPropertyFetch.php
vendored
Normal file
22
vendor/vimeo/psalm/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumPropertyFetch.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Scanner\UnresolvedConstant;
|
||||
|
||||
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
* @internal
|
||||
*/
|
||||
abstract class EnumPropertyFetch extends UnresolvedConstantComponent
|
||||
{
|
||||
public string $fqcln;
|
||||
|
||||
public string $case;
|
||||
|
||||
public function __construct(string $fqcln, string $case)
|
||||
{
|
||||
$this->fqcln = $fqcln;
|
||||
$this->case = $case;
|
||||
}
|
||||
}
|
||||
11
vendor/vimeo/psalm/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumValueFetch.php
vendored
Normal file
11
vendor/vimeo/psalm/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumValueFetch.php
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Scanner\UnresolvedConstant;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
* @internal
|
||||
*/
|
||||
class EnumValueFetch extends EnumPropertyFetch
|
||||
{
|
||||
}
|
||||
@@ -7,8 +7,11 @@ namespace Psalm\Internal\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TNonEmptyArray;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -20,11 +23,14 @@ class ArrayType
|
||||
|
||||
public bool $is_list;
|
||||
|
||||
public function __construct(Union $key, Union $value, bool $is_list)
|
||||
public ?int $count = null;
|
||||
|
||||
public function __construct(Union $key, Union $value, bool $is_list, ?int $count)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->value = $value;
|
||||
$this->is_list = $is_list;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,18 +43,35 @@ class ArrayType
|
||||
public static function infer(Atomic $type): ?self
|
||||
{
|
||||
if ($type instanceof TKeyedArray) {
|
||||
$count = null;
|
||||
if ($type->isSealed()) {
|
||||
$count = count($type->properties);
|
||||
}
|
||||
|
||||
return new self(
|
||||
$type->getGenericKeyType(),
|
||||
$type->getGenericValueType(),
|
||||
$type->is_list,
|
||||
$count,
|
||||
);
|
||||
}
|
||||
|
||||
if ($type instanceof TArray) {
|
||||
if ($type instanceof TNonEmptyArray) {
|
||||
return new self(
|
||||
$type->type_params[0],
|
||||
$type->type_params[1],
|
||||
false,
|
||||
$type->count,
|
||||
);
|
||||
}
|
||||
|
||||
if ($type instanceof TArray) {
|
||||
$empty = $type->isEmptyArray();
|
||||
return new self(
|
||||
$type->type_params[0],
|
||||
$type->type_params[1],
|
||||
false,
|
||||
$empty?0:null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Psalm\Internal\Type\Comparator;
|
||||
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\Scalar;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
@@ -17,15 +18,18 @@ use Psalm\Type\Atomic\TConditional;
|
||||
use Psalm\Type\Atomic\TEmptyMixed;
|
||||
use Psalm\Type\Atomic\TEnumCase;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TIterable;
|
||||
use Psalm\Type\Atomic\TKeyOf;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TList;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNever;
|
||||
use Psalm\Type\Atomic\TNonEmptyArray;
|
||||
use Psalm\Type\Atomic\TNonEmptyMixed;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
use Psalm\Type\Atomic\TObject;
|
||||
use Psalm\Type\Atomic\TObjectWithProperties;
|
||||
@@ -35,12 +39,14 @@ use Psalm\Type\Atomic\TTemplateKeyOf;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TTemplateValueOf;
|
||||
use Psalm\Type\Atomic\TValueOf;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function is_int;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
@@ -81,14 +87,43 @@ class AtomicTypeComparator
|
||||
);
|
||||
}
|
||||
|
||||
if ($input_type_part instanceof TValueOf) {
|
||||
if ($container_type_part instanceof TValueOf) {
|
||||
return UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$input_type_part->type,
|
||||
$container_type_part->type,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
} elseif ($container_type_part instanceof Scalar) {
|
||||
return UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
TValueOf::getValueType($input_type_part->type, $codebase) ?? $input_type_part->type,
|
||||
new Union([$container_type_part]),
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TMixed
|
||||
|| ($container_type_part instanceof TTemplateParam
|
||||
&& $container_type_part->as->isMixed()
|
||||
&& !$container_type_part->extra_types
|
||||
&& $input_type_part instanceof TMixed)
|
||||
) {
|
||||
if (get_class($container_type_part) === TEmptyMixed::class
|
||||
&& get_class($input_type_part) === TMixed::class
|
||||
if (get_class($input_type_part) === TMixed::class
|
||||
&& (
|
||||
get_class($container_type_part) === TEmptyMixed::class
|
||||
|| get_class($container_type_part) === TNonEmptyMixed::class
|
||||
)
|
||||
) {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
@@ -298,7 +333,7 @@ class AtomicTypeComparator
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TEnumCase
|
||||
@@ -600,6 +635,40 @@ class AtomicTypeComparator
|
||||
}
|
||||
}
|
||||
|
||||
if ($input_type_part instanceof TEnumCase
|
||||
&& $codebase->classlike_storage_provider->has($input_type_part->value)
|
||||
) {
|
||||
if ($container_type_part instanceof TString || $container_type_part instanceof TInt) {
|
||||
$input_type_classlike_storage = $codebase->classlike_storage_provider->get($input_type_part->value);
|
||||
if ($input_type_classlike_storage->enum_type === null
|
||||
|| !isset($input_type_classlike_storage->enum_cases[$input_type_part->case_name])
|
||||
) {
|
||||
// Not a backed enum or non-existent enum case
|
||||
return false;
|
||||
}
|
||||
|
||||
$input_type_enum_case_storage = $input_type_classlike_storage->enum_cases[$input_type_part->case_name];
|
||||
assert(
|
||||
$input_type_enum_case_storage->value !== null,
|
||||
'Backed enums cannot have values without a value.',
|
||||
);
|
||||
|
||||
if (is_int($input_type_enum_case_storage->value)) {
|
||||
return self::isContainedBy(
|
||||
$codebase,
|
||||
new TLiteralInt($input_type_enum_case_storage->value),
|
||||
$container_type_part,
|
||||
);
|
||||
}
|
||||
|
||||
return self::isContainedBy(
|
||||
$codebase,
|
||||
Type::getAtomicStringFromLiteral($input_type_enum_case_storage->value),
|
||||
$container_type_part,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TString || $container_type_part instanceof TScalar) {
|
||||
if ($input_type_part instanceof TNamedObject) {
|
||||
// check whether the object has a __toString method
|
||||
|
||||
@@ -80,7 +80,7 @@ class ClassLikeStringComparator
|
||||
: $input_type_part->value,
|
||||
);
|
||||
|
||||
return AtomicTypeComparator::isContainedBy(
|
||||
$isContainedBy = AtomicTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$fake_input_object,
|
||||
$fake_container_object,
|
||||
@@ -88,5 +88,16 @@ class ClassLikeStringComparator
|
||||
false,
|
||||
$atomic_comparison_result,
|
||||
);
|
||||
|
||||
if ($atomic_comparison_result
|
||||
&& $atomic_comparison_result->replacement_atomic_type instanceof TNamedObject
|
||||
) {
|
||||
$atomic_comparison_result->replacement_atomic_type = new TClassString(
|
||||
'object',
|
||||
$atomic_comparison_result->replacement_atomic_type,
|
||||
);
|
||||
}
|
||||
|
||||
return $isContainedBy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
namespace Psalm\Internal\Type\Comparator;
|
||||
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TObjectWithProperties;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_keys;
|
||||
use function is_string;
|
||||
@@ -84,24 +88,47 @@ class KeyedArrayComparator
|
||||
$property_type_comparison,
|
||||
$allow_interface_equality,
|
||||
);
|
||||
if (!$is_input_containedby_container && !$property_type_comparison->type_coerced_from_scalar) {
|
||||
$inverse_property_type_comparison = new TypeComparisonResult();
|
||||
|
||||
if (!$is_input_containedby_container) {
|
||||
if ($atomic_comparison_result) {
|
||||
if (UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$container_property_type,
|
||||
$input_property_type,
|
||||
false,
|
||||
false,
|
||||
$inverse_property_type_comparison,
|
||||
$allow_interface_equality,
|
||||
)
|
||||
|| $inverse_property_type_comparison->type_coerced_from_scalar
|
||||
) {
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
$atomic_comparison_result->type_coerced
|
||||
= $property_type_comparison->type_coerced === true
|
||||
&& $atomic_comparison_result->type_coerced !== false;
|
||||
|
||||
if (!$property_type_comparison->type_coerced_from_scalar
|
||||
&& !$atomic_comparison_result->type_coerced) {
|
||||
//if we didn't detect a coercion, we try to compare the other way around
|
||||
$inverse_property_type_comparison = new TypeComparisonResult();
|
||||
if (UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$container_property_type,
|
||||
$input_property_type,
|
||||
false,
|
||||
false,
|
||||
$inverse_property_type_comparison,
|
||||
$allow_interface_equality,
|
||||
)
|
||||
|| $inverse_property_type_comparison->type_coerced_from_scalar
|
||||
) {
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
}
|
||||
}
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_mixed
|
||||
= $property_type_comparison->type_coerced_from_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_as_mixed
|
||||
= $property_type_comparison->type_coerced_from_as_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_as_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_scalar
|
||||
= $property_type_comparison->type_coerced_from_scalar === true
|
||||
&& $atomic_comparison_result->type_coerced_from_scalar !== false;
|
||||
|
||||
$atomic_comparison_result->scalar_type_match_found
|
||||
= $property_type_comparison->scalar_type_match_found === true
|
||||
&& $atomic_comparison_result->scalar_type_match_found !== false;
|
||||
|
||||
if ($property_type_comparison->missing_shape_fields) {
|
||||
$atomic_comparison_result->missing_shape_fields
|
||||
= $property_type_comparison->missing_shape_fields;
|
||||
@@ -110,13 +137,10 @@ class KeyedArrayComparator
|
||||
|
||||
$all_types_contain = false;
|
||||
} else {
|
||||
if (!$is_input_containedby_container) {
|
||||
$all_types_contain = false;
|
||||
}
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->to_string_cast
|
||||
= $atomic_comparison_result->to_string_cast === true
|
||||
|| $property_type_comparison->to_string_cast === true;
|
||||
|| $property_type_comparison->to_string_cast === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +151,132 @@ class KeyedArrayComparator
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// check remaining $input_properties against container's fallback_params
|
||||
if ($container_type_part instanceof TKeyedArray
|
||||
&& $container_type_part->fallback_params !== null
|
||||
) {
|
||||
[$key_type, $value_type] = $container_type_part->fallback_params;
|
||||
// treat fallback params as possibly undefined
|
||||
// otherwise comparison below would fail for list{0?:int} <=> list{...<int<0,max>, int>}
|
||||
// as the latter `int` is not marked as possibly_undefined
|
||||
$value_type = $value_type->setPossiblyUndefined(true);
|
||||
|
||||
foreach ($input_properties as $key => $input_property_type) {
|
||||
$key_type_comparison = new TypeComparisonResult();
|
||||
if (!UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
is_string($key) ? Type::getString($key) : Type::getInt(false, $key),
|
||||
$key_type,
|
||||
false,
|
||||
false,
|
||||
$key_type_comparison,
|
||||
$allow_interface_equality,
|
||||
)) {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced
|
||||
= $key_type_comparison->type_coerced === true
|
||||
&& $atomic_comparison_result->type_coerced !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_mixed
|
||||
= $key_type_comparison->type_coerced_from_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_as_mixed
|
||||
= $key_type_comparison->type_coerced_from_as_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_as_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_scalar
|
||||
= $key_type_comparison->type_coerced_from_scalar === true
|
||||
&& $atomic_comparison_result->type_coerced_from_scalar !== false;
|
||||
|
||||
$atomic_comparison_result->scalar_type_match_found
|
||||
= $key_type_comparison->scalar_type_match_found === true
|
||||
&& $atomic_comparison_result->scalar_type_match_found !== false;
|
||||
}
|
||||
$all_types_contain = false;
|
||||
}
|
||||
|
||||
$property_type_comparison = new TypeComparisonResult();
|
||||
if (!UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$input_property_type,
|
||||
$value_type,
|
||||
false,
|
||||
false,
|
||||
$property_type_comparison,
|
||||
$allow_interface_equality,
|
||||
)) {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced
|
||||
= $property_type_comparison->type_coerced === true
|
||||
&& $atomic_comparison_result->type_coerced !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_mixed
|
||||
= $property_type_comparison->type_coerced_from_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_as_mixed
|
||||
= $property_type_comparison->type_coerced_from_as_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_as_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_scalar
|
||||
= $property_type_comparison->type_coerced_from_scalar === true
|
||||
&& $atomic_comparison_result->type_coerced_from_scalar !== false;
|
||||
|
||||
$atomic_comparison_result->scalar_type_match_found
|
||||
= $property_type_comparison->scalar_type_match_found === true
|
||||
&& $atomic_comparison_result->scalar_type_match_found !== false;
|
||||
}
|
||||
$all_types_contain = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finally, check input type fallback params against container type fallback params
|
||||
if ($input_type_part instanceof TKeyedArray
|
||||
&& $container_type_part instanceof TKeyedArray
|
||||
&& $input_type_part->fallback_params !== null
|
||||
&& $container_type_part->fallback_params !== null
|
||||
) {
|
||||
foreach ($input_type_part->fallback_params as $i => $input_param) {
|
||||
$container_param = $container_type_part->fallback_params[$i];
|
||||
$param_comparison = new TypeComparisonResult();
|
||||
if (!UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$input_param,
|
||||
$container_param,
|
||||
false,
|
||||
false,
|
||||
$param_comparison,
|
||||
$allow_interface_equality,
|
||||
)) {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced
|
||||
= $param_comparison->type_coerced === true
|
||||
&& $atomic_comparison_result->type_coerced !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_mixed
|
||||
= $param_comparison->type_coerced_from_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_as_mixed
|
||||
= $param_comparison->type_coerced_from_as_mixed === true
|
||||
&& $atomic_comparison_result->type_coerced_from_as_mixed !== false;
|
||||
|
||||
$atomic_comparison_result->type_coerced_from_scalar
|
||||
= $param_comparison->type_coerced_from_scalar === true
|
||||
&& $atomic_comparison_result->type_coerced_from_scalar !== false;
|
||||
|
||||
$atomic_comparison_result->scalar_type_match_found
|
||||
= $param_comparison->scalar_type_match_found === true
|
||||
&& $atomic_comparison_result->scalar_type_match_found !== false;
|
||||
}
|
||||
$all_types_contain = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $all_types_contain;
|
||||
}
|
||||
|
||||
@@ -139,36 +289,20 @@ class KeyedArrayComparator
|
||||
): bool {
|
||||
$all_types_contain = true;
|
||||
|
||||
$input_object_with_keys = self::coerceToObjectWithProperties(
|
||||
$codebase,
|
||||
$input_type_part,
|
||||
$container_type_part,
|
||||
);
|
||||
|
||||
foreach ($container_type_part->properties as $property_name => $container_property_type) {
|
||||
if (!is_string($property_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$codebase->classlikes->classOrInterfaceExists($input_type_part->value)) {
|
||||
if (!$input_object_with_keys || !isset($input_object_with_keys->properties[$property_name])) {
|
||||
$all_types_contain = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$codebase->properties->propertyExists(
|
||||
$input_type_part->value . '::$' . $property_name,
|
||||
true,
|
||||
)) {
|
||||
$all_types_contain = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$property_declaring_class = (string) $codebase->properties->getDeclaringClassForProperty(
|
||||
$input_type_part . '::$' . $property_name,
|
||||
true,
|
||||
);
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($property_declaring_class);
|
||||
|
||||
$input_property_storage = $class_storage->properties[$property_name];
|
||||
|
||||
$input_property_type = $input_property_storage->type ?: Type::getMixed();
|
||||
$input_property_type = $input_object_with_keys->properties[$property_name];
|
||||
|
||||
$property_type_comparison = new TypeComparisonResult();
|
||||
|
||||
@@ -208,4 +342,61 @@ class KeyedArrayComparator
|
||||
|
||||
return $all_types_contain;
|
||||
}
|
||||
|
||||
public static function coerceToObjectWithProperties(
|
||||
Codebase $codebase,
|
||||
TNamedObject $input_type_part,
|
||||
TObjectWithProperties $container_type_part
|
||||
): ?TObjectWithProperties {
|
||||
$storage = $codebase->classlikes->getStorageFor($input_type_part->value);
|
||||
|
||||
if (!$storage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$inferred_lower_bounds = [];
|
||||
|
||||
if ($input_type_part instanceof TGenericObject) {
|
||||
foreach ($storage->template_types ?? [] as $template_name => $templates) {
|
||||
foreach (array_keys($templates) as $offset => $defining_at) {
|
||||
$inferred_lower_bounds[$template_name][$defining_at] =
|
||||
$input_type_part->type_params[$offset];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($storage->template_extended_params ?? [] as $defining_at => $templates) {
|
||||
foreach ($templates as $template_name => $template_atomic) {
|
||||
$inferred_lower_bounds[$template_name][$defining_at] = $template_atomic;
|
||||
}
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
|
||||
foreach ($storage->appearing_property_ids as $property_name => $property_id) {
|
||||
if (!isset($container_type_part->properties[$property_name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$property_type = $codebase->properties->hasStorage($property_id)
|
||||
? $codebase->properties->getStorage($property_id)->type
|
||||
: null;
|
||||
|
||||
$properties[$property_name] = $property_type ?? Type::getMixed();
|
||||
}
|
||||
|
||||
$replaced_object = TemplateInferredTypeReplacer::replace(
|
||||
new Union([
|
||||
new TObjectWithProperties($properties),
|
||||
]),
|
||||
new TemplateResult(
|
||||
$storage->template_types ?? [],
|
||||
$inferred_lower_bounds,
|
||||
),
|
||||
$codebase,
|
||||
);
|
||||
|
||||
/** @var TObjectWithProperties */
|
||||
return $replaced_object->getSingleAtomic();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Psalm\Internal\Type\Comparator;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TCallableObject;
|
||||
use Psalm\Type\Atomic\TIterable;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
@@ -90,6 +91,8 @@ class ObjectComparator
|
||||
$intersection_container_type_lower = 'object';
|
||||
} elseif ($intersection_container_type instanceof TTemplateParam) {
|
||||
$intersection_container_type_lower = null;
|
||||
} elseif ($intersection_container_type instanceof TCallableObject) {
|
||||
$intersection_container_type_lower = 'callable-object';
|
||||
} else {
|
||||
$container_was_static = $intersection_container_type->is_static;
|
||||
|
||||
@@ -134,7 +137,7 @@ class ObjectComparator
|
||||
|
||||
/**
|
||||
* @param TNamedObject|TTemplateParam|TIterable $type_part
|
||||
* @return array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties>
|
||||
* @return array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject>
|
||||
*/
|
||||
private static function getIntersectionTypes(Atomic $type_part): array
|
||||
{
|
||||
@@ -166,8 +169,8 @@ class ObjectComparator
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $intersection_input_type
|
||||
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $intersection_container_type
|
||||
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject $intersection_input_type
|
||||
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject $intersection_container_type
|
||||
*/
|
||||
private static function isIntersectionShallowlyContainedBy(
|
||||
Codebase $codebase,
|
||||
@@ -268,6 +271,8 @@ class ObjectComparator
|
||||
$intersection_input_type_lower = 'iterable';
|
||||
} elseif ($intersection_input_type instanceof TObjectWithProperties) {
|
||||
$intersection_input_type_lower = 'object';
|
||||
} elseif ($intersection_input_type instanceof TCallableObject) {
|
||||
$intersection_input_type_lower = 'callable-object';
|
||||
} else {
|
||||
$input_was_static = $intersection_input_type->is_static;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class UnionTypeComparator
|
||||
bool $allow_interface_equality = false,
|
||||
bool $allow_float_int_equality = true
|
||||
): bool {
|
||||
if ($container_type->isMixed()) {
|
||||
if ($container_type->isVanillaMixed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -63,9 +63,6 @@ class UnionTypeComparator
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($container_type->hasMixed() && !$container_type->isEmptyMixed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$container_has_template = $container_type->hasTemplateOrStatic();
|
||||
|
||||
@@ -178,7 +175,7 @@ class UnionTypeComparator
|
||||
if ($container_required_param_count > $input_all_param_count
|
||||
|| $container_all_param_count < $input_required_param_count
|
||||
) {
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,10 @@ class NegatedAssertionReconciler extends Reconciler
|
||||
) {
|
||||
$existing_var_type->removeType('array-key');
|
||||
$existing_var_type->addType(new TString);
|
||||
} elseif ($assertion_type instanceof TNonEmptyString
|
||||
&& $existing_var_type->hasString()
|
||||
) {
|
||||
// do nothing
|
||||
} elseif ($assertion instanceof IsClassNotEqual) {
|
||||
// do nothing
|
||||
} elseif ($assertion_type instanceof TClassString && $assertion_type->is_loaded) {
|
||||
|
||||
@@ -64,11 +64,14 @@ class ParseTreeCreator
|
||||
$type_token = $this->type_tokens[$this->t];
|
||||
|
||||
switch ($type_token[0]) {
|
||||
case '<':
|
||||
case '{':
|
||||
case ']':
|
||||
throw new TypeParseTreeException('Unexpected token ' . $type_token[0]);
|
||||
|
||||
case '<':
|
||||
$this->handleLessThan();
|
||||
break;
|
||||
|
||||
case '[':
|
||||
$this->handleOpenSquareBracket();
|
||||
break;
|
||||
@@ -232,6 +235,29 @@ class ParseTreeCreator
|
||||
$this->current_leaf = $new_parent_leaf;
|
||||
}
|
||||
|
||||
private function handleLessThan(): void
|
||||
{
|
||||
if (!$this->current_leaf instanceof FieldEllipsis) {
|
||||
throw new TypeParseTreeException('Unexpected token <');
|
||||
}
|
||||
|
||||
$current_parent = $this->current_leaf->parent;
|
||||
|
||||
if (!$current_parent instanceof KeyedArrayTree) {
|
||||
throw new TypeParseTreeException('Unexpected token <');
|
||||
}
|
||||
|
||||
array_pop($current_parent->children);
|
||||
|
||||
$generic_leaf = new GenericTree(
|
||||
'',
|
||||
$current_parent,
|
||||
);
|
||||
$current_parent->children []= $generic_leaf;
|
||||
|
||||
$this->current_leaf = $generic_leaf;
|
||||
}
|
||||
|
||||
private function handleOpenSquareBracket(): void
|
||||
{
|
||||
if ($this->current_leaf instanceof Root) {
|
||||
|
||||
@@ -28,6 +28,7 @@ use Psalm\Storage\Assertion\NonEmpty;
|
||||
use Psalm\Storage\Assertion\NonEmptyCountable;
|
||||
use Psalm\Storage\Assertion\Truthy;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\Scalar;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TArrayKey;
|
||||
@@ -71,11 +72,13 @@ use Psalm\Type\Atomic\TScalar;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TTrue;
|
||||
use Psalm\Type\Atomic\TValueOf;
|
||||
use Psalm\Type\Reconciler;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function explode;
|
||||
@@ -290,7 +293,9 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
|
||||
if ($assertion_type instanceof TObject) {
|
||||
return self::reconcileObject(
|
||||
$codebase,
|
||||
$assertion,
|
||||
$assertion_type,
|
||||
$existing_var_type,
|
||||
$key,
|
||||
$negated,
|
||||
@@ -527,6 +532,10 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
}
|
||||
}
|
||||
|
||||
if ($assertion_type instanceof TValueOf) {
|
||||
return $assertion_type->type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1574,7 +1583,9 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
* @param Reconciler::RECONCILIATION_* $failed_reconciliation
|
||||
*/
|
||||
private static function reconcileObject(
|
||||
Codebase $codebase,
|
||||
Assertion $assertion,
|
||||
TObject $assertion_type,
|
||||
Union $existing_var_type,
|
||||
?string $key,
|
||||
bool $negated,
|
||||
@@ -1593,11 +1604,25 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
$object_types = [];
|
||||
$redundant = true;
|
||||
|
||||
$assertion_type_is_intersectable_type = Type::isIntersectionType($assertion_type);
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if ($type->isObjectType()) {
|
||||
$object_types[] = $type;
|
||||
if ($assertion_type_is_intersectable_type
|
||||
&& self::areIntersectionTypesAllowed($codebase, $type)
|
||||
) {
|
||||
/** @var TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject $assertion_type */
|
||||
$object_types[] = $type->addIntersectionType($assertion_type);
|
||||
$redundant = false;
|
||||
} elseif ($type->isObjectType()) {
|
||||
if ($assertion_type_is_intersectable_type
|
||||
&& !self::areIntersectionTypesAllowed($codebase, $type)
|
||||
) {
|
||||
$redundant = false;
|
||||
} else {
|
||||
$object_types[] = $type;
|
||||
}
|
||||
} elseif ($type instanceof TCallable) {
|
||||
$object_types[] = new TCallableObject();
|
||||
$callable_object = new TCallableObject($type->from_docblock, $type);
|
||||
$object_types[] = $callable_object;
|
||||
$redundant = false;
|
||||
} elseif ($type instanceof TTemplateParam
|
||||
&& $type->as->isMixed()
|
||||
@@ -1607,8 +1632,17 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
$redundant = false;
|
||||
} elseif ($type instanceof TTemplateParam) {
|
||||
if ($type->as->hasObject() || $type->as->hasMixed()) {
|
||||
$type = $type->replaceAs(self::reconcileObject(
|
||||
/**
|
||||
* @psalm-suppress PossiblyInvalidArgument This looks wrong, psalm assumes that $assertion_type
|
||||
* can contain TNamedObject due to the reconciliation above
|
||||
* regarding {@see Type::isIntersectionType}. Due to the
|
||||
* native argument type `TObject`, the variable object will
|
||||
* never be `TNamedObject`.
|
||||
*/
|
||||
$reconciled_type = self::reconcileObject(
|
||||
$codebase,
|
||||
$assertion,
|
||||
$assertion_type,
|
||||
$type->as,
|
||||
null,
|
||||
false,
|
||||
@@ -1616,7 +1650,8 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
$suppressed_issues,
|
||||
$failed_reconciliation,
|
||||
$is_equality,
|
||||
));
|
||||
);
|
||||
$type = $type->replaceAs($reconciled_type);
|
||||
|
||||
$object_types[] = $type;
|
||||
}
|
||||
@@ -2894,19 +2929,41 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
int &$failed_reconciliation
|
||||
): Union {
|
||||
$class_name = $class_constant_expression->fq_classlike_name;
|
||||
$constant_pattern = $class_constant_expression->const_name;
|
||||
|
||||
$resolver = new ClassConstantByWildcardResolver($codebase);
|
||||
$matched_class_constant_types = $resolver->resolve($class_name, $constant_pattern);
|
||||
if ($matched_class_constant_types === null) {
|
||||
if (!$codebase->classlike_storage_provider->has($class_name)) {
|
||||
return $existing_type;
|
||||
}
|
||||
|
||||
if ($matched_class_constant_types === []) {
|
||||
$constant_pattern = $class_constant_expression->const_name;
|
||||
|
||||
$resolver = new ClassConstantByWildcardResolver($codebase);
|
||||
$matched_class_constant_types = $resolver->resolve(
|
||||
$class_name,
|
||||
$constant_pattern,
|
||||
);
|
||||
|
||||
if ($matched_class_constant_types === null) {
|
||||
$failed_reconciliation = Reconciler::RECONCILIATION_EMPTY;
|
||||
return Type::getNever();
|
||||
}
|
||||
|
||||
return TypeCombiner::combine($matched_class_constant_types, $codebase);
|
||||
return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type
|
||||
*/
|
||||
private static function areIntersectionTypesAllowed(Codebase $codebase, Atomic $type): bool
|
||||
{
|
||||
if ($type instanceof TObjectWithProperties || $type instanceof TCallableObject) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$type instanceof TNamedObject || !$codebase->classlike_storage_provider->has($type->value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($type->value);
|
||||
|
||||
return !$class_storage->final;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Psalm\Codebase;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\Methods;
|
||||
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
|
||||
use Psalm\Internal\Type\Comparator\KeyedArrayComparator;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
@@ -33,7 +34,6 @@ use Psalm\Type\Atomic\TTemplateParamClass;
|
||||
use Psalm\Type\Atomic\TTemplatePropertiesOf;
|
||||
use Psalm\Type\Atomic\TTemplateValueOf;
|
||||
use Psalm\Type\Union;
|
||||
use Throwable;
|
||||
|
||||
use function array_fill;
|
||||
use function array_keys;
|
||||
@@ -231,6 +231,17 @@ class TemplateStandinTypeReplacer
|
||||
);
|
||||
}
|
||||
|
||||
if ($atomic_type instanceof TTemplateParam
|
||||
&& isset($template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class])
|
||||
) {
|
||||
$most_specific_type = self::getMostSpecificTypeFromBounds(
|
||||
$template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class],
|
||||
$codebase,
|
||||
);
|
||||
|
||||
return array_values($most_specific_type->getAtomicTypes());
|
||||
}
|
||||
|
||||
if ($atomic_type instanceof TTemplateParamClass
|
||||
&& isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])
|
||||
) {
|
||||
@@ -259,11 +270,14 @@ class TemplateStandinTypeReplacer
|
||||
|
||||
$include_first = true;
|
||||
|
||||
if (isset($template_result->template_types[$atomic_type->array_param_name][$atomic_type->defining_class])
|
||||
if (isset($template_result->lower_bounds[$atomic_type->array_param_name][$atomic_type->defining_class])
|
||||
&& !empty($template_result->lower_bounds[$atomic_type->offset_param_name])
|
||||
) {
|
||||
$array_template_type
|
||||
= $template_result->template_types[$atomic_type->array_param_name][$atomic_type->defining_class];
|
||||
= self::getMostSpecificTypeFromBounds(
|
||||
$template_result->lower_bounds[$atomic_type->array_param_name][$atomic_type->defining_class],
|
||||
$codebase,
|
||||
);
|
||||
$offset_template_type
|
||||
= self::getMostSpecificTypeFromBounds(
|
||||
array_values($template_result->lower_bounds[$atomic_type->offset_param_name])[0],
|
||||
@@ -317,10 +331,18 @@ class TemplateStandinTypeReplacer
|
||||
$atomic_types = [];
|
||||
|
||||
$include_first = true;
|
||||
$template_type = null;
|
||||
|
||||
if (isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])) {
|
||||
if (isset($template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class])) {
|
||||
$template_type = self::getMostSpecificTypeFromBounds(
|
||||
$template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class],
|
||||
$codebase,
|
||||
);
|
||||
} elseif (isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])) {
|
||||
$template_type = $template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class];
|
||||
}
|
||||
|
||||
if ($template_type) {
|
||||
foreach ($template_type->getAtomicTypes() as $template_atomic) {
|
||||
if ($template_atomic instanceof TList) {
|
||||
$template_atomic = $template_atomic->getKeyedArray();
|
||||
@@ -563,7 +585,7 @@ class TemplateStandinTypeReplacer
|
||||
|
||||
if (!empty($classlike_storage->template_extended_params[$base_type->value])) {
|
||||
$atomic_input_type = new TGenericObject(
|
||||
$atomic_input_type->value,
|
||||
$base_type->value,
|
||||
array_values($classlike_storage->template_extended_params[$base_type->value]),
|
||||
);
|
||||
|
||||
@@ -582,6 +604,22 @@ class TemplateStandinTypeReplacer
|
||||
}
|
||||
}
|
||||
|
||||
if ($atomic_input_type instanceof TNamedObject
|
||||
&& $base_type instanceof TObjectWithProperties
|
||||
) {
|
||||
$object_with_keys = KeyedArrayComparator::coerceToObjectWithProperties(
|
||||
$codebase,
|
||||
$atomic_input_type,
|
||||
$base_type,
|
||||
);
|
||||
|
||||
if ($object_with_keys) {
|
||||
$matching_atomic_types[$object_with_keys->getId()] = $object_with_keys;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($atomic_input_type instanceof TTemplateParam) {
|
||||
$matching_atomic_types = array_merge(
|
||||
$matching_atomic_types,
|
||||
@@ -1233,13 +1271,13 @@ class TemplateStandinTypeReplacer
|
||||
$input_type_params = [];
|
||||
}
|
||||
|
||||
try {
|
||||
$input_class_storage = $codebase->classlike_storage_provider->get($input_type_part->value);
|
||||
$container_class_storage = $codebase->classlike_storage_provider->get($container_type_part->value);
|
||||
$container_type_params_covariant = $container_class_storage->template_covariants;
|
||||
} catch (Throwable $e) {
|
||||
$input_class_storage = null;
|
||||
}
|
||||
$input_class_storage = $codebase->classlike_storage_provider->has($input_type_part->value)
|
||||
? $codebase->classlike_storage_provider->get($input_type_part->value)
|
||||
: null;
|
||||
|
||||
$container_type_params_covariant = $codebase->classlike_storage_provider->has($container_type_part->value)
|
||||
? $codebase->classlike_storage_provider->get($container_type_part->value)->template_covariants
|
||||
: null;
|
||||
|
||||
if ($input_type_part->value !== $container_type_part->value
|
||||
&& $input_class_storage
|
||||
@@ -1266,8 +1304,12 @@ class TemplateStandinTypeReplacer
|
||||
|
||||
$template_extends = $input_class_storage->template_extended_params;
|
||||
|
||||
if (isset($template_extends[$container_type_part->value])) {
|
||||
$params = $template_extends[$container_type_part->value];
|
||||
$container_type_part_value = $container_type_part->value === 'iterable'
|
||||
? 'Traversable'
|
||||
: $container_type_part->value;
|
||||
|
||||
if (isset($template_extends[$container_type_part_value])) {
|
||||
$params = $template_extends[$container_type_part_value];
|
||||
|
||||
$new_input_params = [];
|
||||
|
||||
|
||||
@@ -551,6 +551,8 @@ class TypeCombiner
|
||||
}
|
||||
|
||||
foreach ($type->type_params as $i => $type_param) {
|
||||
// See https://github.com/vimeo/psalm/pull/9439#issuecomment-1464563015
|
||||
/** @psalm-suppress PropertyTypeCoercion */
|
||||
$combination->array_type_params[$i] = Type::combineUnionTypes(
|
||||
$combination->array_type_params[$i] ?? null,
|
||||
$type_param,
|
||||
@@ -599,6 +601,8 @@ class TypeCombiner
|
||||
|
||||
if ($type instanceof TClassStringMap) {
|
||||
foreach ([$type->getStandinKeyParam(), $type->value_param] as $i => $type_param) {
|
||||
// See https://github.com/vimeo/psalm/pull/9439#issuecomment-1464563015
|
||||
/** @psalm-suppress PropertyTypeCoercion */
|
||||
$combination->array_type_params[$i] = Type::combineUnionTypes(
|
||||
$combination->array_type_params[$i] ?? null,
|
||||
$type_param,
|
||||
@@ -1047,19 +1051,25 @@ class TypeCombiner
|
||||
if (!isset($combination->value_types['string'])) {
|
||||
if ($combination->strings) {
|
||||
if ($type instanceof TNumericString) {
|
||||
$has_non_numeric_string = false;
|
||||
$has_only_numeric_strings = true;
|
||||
$has_only_non_empty_strings = true;
|
||||
|
||||
foreach ($combination->strings as $string_type) {
|
||||
if (!is_numeric($string_type->value)) {
|
||||
$has_non_numeric_string = true;
|
||||
break;
|
||||
$has_only_numeric_strings = false;
|
||||
}
|
||||
|
||||
if ($string_type->value === '') {
|
||||
$has_only_non_empty_strings = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($has_non_numeric_string) {
|
||||
$combination->value_types['string'] = new TString();
|
||||
} else {
|
||||
if ($has_only_numeric_strings) {
|
||||
$combination->value_types['string'] = $type;
|
||||
} elseif ($has_only_non_empty_strings) {
|
||||
$combination->value_types['string'] = new TNonEmptyString();
|
||||
} else {
|
||||
$combination->value_types['string'] = new TString();
|
||||
}
|
||||
} elseif ($type instanceof TLowercaseString) {
|
||||
$has_non_lowercase_string = false;
|
||||
|
||||
@@ -6,9 +6,6 @@ use Psalm\Codebase;
|
||||
use Psalm\Exception\CircularReferenceException;
|
||||
use Psalm\Exception\UnresolvableConstantException;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer;
|
||||
use Psalm\Internal\Type\SimpleAssertionReconciler;
|
||||
use Psalm\Internal\Type\SimpleNegatedAssertionReconciler;
|
||||
use Psalm\Internal\Type\TypeParser;
|
||||
use Psalm\Storage\Assertion\IsType;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
@@ -18,6 +15,7 @@ use Psalm\Type\Atomic\TClassConstant;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Atomic\TConditional;
|
||||
use Psalm\Type\Atomic\TEnumCase;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TIntMask;
|
||||
@@ -41,7 +39,6 @@ use Psalm\Type\Union;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
@@ -49,9 +46,7 @@ use function count;
|
||||
use function get_class;
|
||||
use function is_string;
|
||||
use function reset;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -77,8 +72,6 @@ class TypeExpander
|
||||
): Union {
|
||||
$new_return_type_parts = [];
|
||||
|
||||
$had_split_values = false;
|
||||
|
||||
foreach ($return_type->getAtomicTypes() as $return_type_part) {
|
||||
$parts = self::expandAtomic(
|
||||
$codebase,
|
||||
@@ -94,21 +87,13 @@ class TypeExpander
|
||||
$throw_on_unresolvable_constant,
|
||||
);
|
||||
|
||||
if ($return_type_part instanceof TTypeAlias || count($parts) > 1) {
|
||||
$had_split_values = true;
|
||||
}
|
||||
|
||||
$new_return_type_parts = [...$new_return_type_parts, ...$parts];
|
||||
}
|
||||
|
||||
if ($had_split_values) {
|
||||
$fleshed_out_type = TypeCombiner::combine(
|
||||
$new_return_type_parts,
|
||||
$codebase,
|
||||
);
|
||||
} else {
|
||||
$fleshed_out_type = new Union($new_return_type_parts);
|
||||
}
|
||||
$fleshed_out_type = TypeCombiner::combine(
|
||||
$new_return_type_parts,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
$fleshed_out_type->from_docblock = $return_type->from_docblock;
|
||||
$fleshed_out_type->ignore_nullable_issues = $return_type->ignore_nullable_issues;
|
||||
@@ -146,6 +131,10 @@ class TypeExpander
|
||||
bool $expand_templates = false,
|
||||
bool $throw_on_unresolvable_constant = false
|
||||
): array {
|
||||
if ($return_type instanceof TEnumCase) {
|
||||
return [$return_type];
|
||||
}
|
||||
|
||||
if ($return_type instanceof TNamedObject
|
||||
|| $return_type instanceof TTemplateParam
|
||||
) {
|
||||
@@ -260,52 +249,18 @@ class TypeExpander
|
||||
return [new TLiteralClassString($return_type->fq_classlike_name)];
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($return_type->fq_classlike_name);
|
||||
|
||||
if (strpos($return_type->const_name, '*') !== false) {
|
||||
$matching_constants = [
|
||||
...array_keys($class_storage->constants),
|
||||
...array_keys($class_storage->enum_cases),
|
||||
];
|
||||
|
||||
$const_name_part = substr($return_type->const_name, 0, -1);
|
||||
|
||||
if ($const_name_part) {
|
||||
$matching_constants = array_filter(
|
||||
$matching_constants,
|
||||
static fn($constant_name): bool => $constant_name !== $const_name_part
|
||||
&& strpos($constant_name, $const_name_part) === 0
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$matching_constants = [$return_type->const_name];
|
||||
try {
|
||||
$class_constant = $codebase->classlikes->getClassConstantType(
|
||||
$return_type->fq_classlike_name,
|
||||
$return_type->const_name,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
);
|
||||
} catch (CircularReferenceException $e) {
|
||||
$class_constant = null;
|
||||
}
|
||||
|
||||
$matching_constant_types = [];
|
||||
|
||||
foreach ($matching_constants as $matching_constant) {
|
||||
try {
|
||||
$class_constant = $codebase->classlikes->getClassConstantType(
|
||||
$return_type->fq_classlike_name,
|
||||
$matching_constant,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
);
|
||||
} catch (CircularReferenceException $e) {
|
||||
$class_constant = null;
|
||||
}
|
||||
|
||||
if ($class_constant) {
|
||||
if ($class_constant->isSingle()) {
|
||||
$matching_constant_types = array_merge(
|
||||
array_values($class_constant->getAtomicTypes()),
|
||||
$matching_constant_types,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($matching_constant_types) {
|
||||
return $matching_constant_types;
|
||||
if ($class_constant) {
|
||||
return array_values($class_constant->getAtomicTypes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +324,7 @@ class TypeExpander
|
||||
];
|
||||
}
|
||||
|
||||
/** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */
|
||||
foreach ($return_type->extra_types ?? [] as $alias) {
|
||||
$more_recursively_fleshed_out_types = self::expandAtomic(
|
||||
$codebase,
|
||||
@@ -1102,7 +1058,7 @@ class TypeExpander
|
||||
}
|
||||
|
||||
if ($throw_on_unresolvable_constant
|
||||
&& !$codebase->classOrInterfaceExists($type_param->fq_classlike_name)
|
||||
&& !$codebase->classOrInterfaceOrEnumExists($type_param->fq_classlike_name)
|
||||
) {
|
||||
throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name);
|
||||
}
|
||||
@@ -1112,6 +1068,10 @@ class TypeExpander
|
||||
$type_param->fq_classlike_name,
|
||||
$type_param->const_name,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
null,
|
||||
[],
|
||||
false,
|
||||
$return_type instanceof TValueOf,
|
||||
);
|
||||
} catch (CircularReferenceException $e) {
|
||||
return [$return_type];
|
||||
@@ -1148,9 +1108,11 @@ class TypeExpander
|
||||
} else {
|
||||
$new_return_types = TValueOf::getValueType(new Union($type_atomics), $codebase);
|
||||
}
|
||||
|
||||
if ($new_return_types === null) {
|
||||
return [$return_type];
|
||||
}
|
||||
|
||||
return array_values($new_return_types->getAtomicTypes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TArrayKey;
|
||||
use Psalm\Type\Atomic\TCallable;
|
||||
use Psalm\Type\Atomic\TCallableKeyedArray;
|
||||
use Psalm\Type\Atomic\TCallableObject;
|
||||
use Psalm\Type\Atomic\TClassConstant;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TClassStringMap;
|
||||
@@ -61,6 +62,7 @@ use Psalm\Type\Atomic\TTemplateParamClass;
|
||||
use Psalm\Type\Atomic\TTemplatePropertiesOf;
|
||||
use Psalm\Type\Atomic\TTemplateValueOf;
|
||||
use Psalm\Type\Atomic\TTypeAlias;
|
||||
use Psalm\Type\Atomic\TUnknownClassString;
|
||||
use Psalm\Type\Atomic\TValueOf;
|
||||
use Psalm\Type\TypeNode;
|
||||
use Psalm\Type\Union;
|
||||
@@ -69,6 +71,7 @@ use function array_key_exists;
|
||||
use function array_key_first;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function array_shift;
|
||||
use function array_unique;
|
||||
@@ -91,6 +94,7 @@ use function stripslashes;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function strtr;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
@@ -420,8 +424,8 @@ class TypeParser
|
||||
return new TLiteralFloat((float) $parse_tree->value, $from_docblock);
|
||||
}
|
||||
|
||||
if (preg_match('/^\-?(0|[1-9][0-9]*)$/', $parse_tree->value)) {
|
||||
return new TLiteralInt((int) $parse_tree->value, $from_docblock);
|
||||
if (preg_match('/^\-?(0|[1-9]([0-9_]*[0-9])?)$/', $parse_tree->value)) {
|
||||
return new TLiteralInt((int) strtr($parse_tree->value, ['_' => '']), $from_docblock);
|
||||
}
|
||||
|
||||
if (!preg_match('@^(\$this|\\\\?[a-zA-Z_\x7f-\xff][\\\\\-0-9a-zA-Z_\x7f-\xff]*)$@', $parse_tree->value)) {
|
||||
@@ -684,6 +688,9 @@ class TypeParser
|
||||
}
|
||||
|
||||
if ($generic_type_value === 'list') {
|
||||
if (count($generic_params) > 1) {
|
||||
throw new TypeParseTreeException('Too many template parameters for list');
|
||||
}
|
||||
return Type::getListAtomic($generic_params[0], $from_docblock);
|
||||
}
|
||||
|
||||
@@ -710,13 +717,25 @@ class TypeParser
|
||||
|
||||
$types = [];
|
||||
foreach ($generic_params[0]->getAtomicTypes() as $type) {
|
||||
if (!$type instanceof TNamedObject) {
|
||||
throw new TypeParseTreeException('Class string param should be a named object');
|
||||
if ($type instanceof TNamedObject) {
|
||||
$types[] = new TClassString($type->value, $type, false, false, false, $from_docblock);
|
||||
continue;
|
||||
}
|
||||
|
||||
$types[] = new TClassString($type->value, $type, false, false, false, $from_docblock);
|
||||
if ($type instanceof TCallableObject) {
|
||||
$types[] = new TUnknownClassString($type, false, $from_docblock);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new TypeParseTreeException('class-string param can only target to named or callable objects');
|
||||
}
|
||||
|
||||
assert(
|
||||
$types !== [],
|
||||
'Since `Union` cannot be empty and all non-supported atomics lead to thrown exception,'
|
||||
.' we can safely assert that the types array is non-empty.',
|
||||
);
|
||||
|
||||
return new Union($types);
|
||||
}
|
||||
|
||||
@@ -778,7 +797,7 @@ class TypeParser
|
||||
if ($template_param->getIntersectionTypes()) {
|
||||
throw new TypeParseTreeException(
|
||||
$generic_type_value . '<' . $param_name . '> must be a TTemplateParam'
|
||||
. ' with no intersection types.',
|
||||
. ' with no intersection types.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1093,6 +1112,10 @@ class TypeParser
|
||||
$intersection_types[$name] = $atomic_type;
|
||||
}
|
||||
|
||||
if ($intersection_types === []) {
|
||||
return new TMixed();
|
||||
}
|
||||
|
||||
$first_type = reset($intersection_types);
|
||||
$last_type = end($intersection_types);
|
||||
|
||||
@@ -1112,129 +1135,64 @@ class TypeParser
|
||||
}
|
||||
|
||||
if ($onlyTKeyedArray) {
|
||||
/** @var non-empty-array<string|int, Union> */
|
||||
$properties = [];
|
||||
|
||||
if ($first_type instanceof TArray) {
|
||||
array_shift($intersection_types);
|
||||
} elseif ($last_type instanceof TArray) {
|
||||
array_pop($intersection_types);
|
||||
}
|
||||
|
||||
$all_sealed = true;
|
||||
|
||||
/** @var TKeyedArray $intersection_type */
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
foreach ($intersection_type->properties as $property => $property_type) {
|
||||
if ($intersection_type->fallback_params !== null) {
|
||||
$all_sealed = false;
|
||||
}
|
||||
|
||||
if (!array_key_exists($property, $properties)) {
|
||||
$properties[$property] = $property_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_type = Type::intersectUnionTypes(
|
||||
$properties[$property],
|
||||
$property_type,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if ($new_type === null) {
|
||||
throw new TypeParseTreeException(
|
||||
'Incompatible intersection types for "' . $property . '", '
|
||||
. $properties[$property] . ' and ' . $property_type
|
||||
. ' provided',
|
||||
);
|
||||
}
|
||||
$properties[$property] = $new_type;
|
||||
}
|
||||
}
|
||||
|
||||
$first_or_last_type = $first_type instanceof TArray
|
||||
? $first_type
|
||||
: ($last_type instanceof TArray ? $last_type : null);
|
||||
|
||||
$fallback_params = null;
|
||||
|
||||
if ($first_or_last_type !== null) {
|
||||
$fallback_params = [
|
||||
$first_or_last_type->type_params[0],
|
||||
$first_or_last_type->type_params[1],
|
||||
];
|
||||
} elseif (!$all_sealed) {
|
||||
$fallback_params = [Type::getArrayKey(), Type::getMixed()];
|
||||
}
|
||||
|
||||
return new TKeyedArray(
|
||||
$properties,
|
||||
null,
|
||||
$fallback_params,
|
||||
false,
|
||||
/**
|
||||
* @var array<TKeyedArray> $intersection_types
|
||||
* @var TKeyedArray $first_type
|
||||
* @var TKeyedArray $last_type
|
||||
*/
|
||||
return self::getTypeFromKeyedArrays(
|
||||
$codebase,
|
||||
$intersection_types,
|
||||
$first_type,
|
||||
$last_type,
|
||||
$from_docblock,
|
||||
);
|
||||
}
|
||||
|
||||
$keyed_intersection_types = [];
|
||||
$keyed_intersection_types = self::extractKeyedIntersectionTypes(
|
||||
$codebase,
|
||||
$intersection_types,
|
||||
);
|
||||
|
||||
if ($intersection_types[0] instanceof TTypeAlias) {
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if (!$intersection_type instanceof TTypeAlias) {
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types with a type alias can only be comprised of other type aliases, '
|
||||
. get_class($intersection_type) . ' provided',
|
||||
);
|
||||
}
|
||||
$intersect_static = false;
|
||||
|
||||
$keyed_intersection_types[$intersection_type->getKey()] = $intersection_type;
|
||||
}
|
||||
if (isset($keyed_intersection_types['static'])) {
|
||||
unset($keyed_intersection_types['static']);
|
||||
$intersect_static = true;
|
||||
}
|
||||
|
||||
$first_type = array_shift($keyed_intersection_types);
|
||||
if ($keyed_intersection_types === [] && $intersect_static) {
|
||||
return new TNamedObject('static', false, false, [], $from_docblock);
|
||||
}
|
||||
|
||||
if ($keyed_intersection_types) {
|
||||
return $first_type->setIntersectionTypes($keyed_intersection_types);
|
||||
}
|
||||
} else {
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if (!$intersection_type instanceof TIterable
|
||||
&& !$intersection_type instanceof TNamedObject
|
||||
&& !$intersection_type instanceof TTemplateParam
|
||||
&& !$intersection_type instanceof TObjectWithProperties
|
||||
) {
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types must be all objects, '
|
||||
. get_class($intersection_type) . ' provided',
|
||||
);
|
||||
}
|
||||
$first_type = array_shift($keyed_intersection_types);
|
||||
|
||||
$keyed_intersection_types[$intersection_type instanceof TIterable
|
||||
? $intersection_type->getId()
|
||||
: $intersection_type->getKey()] = $intersection_type;
|
||||
}
|
||||
// Keyed array intersection are merged together and are not combinable with object-types
|
||||
if ($first_type instanceof TKeyedArray) {
|
||||
// assume all types are keyed arrays
|
||||
array_unshift($keyed_intersection_types, $first_type);
|
||||
/** @var TKeyedArray $last_type */
|
||||
$last_type = end($keyed_intersection_types);
|
||||
|
||||
$intersect_static = false;
|
||||
/** @var array<TKeyedArray> $keyed_intersection_types */
|
||||
return self::getTypeFromKeyedArrays(
|
||||
$codebase,
|
||||
$keyed_intersection_types,
|
||||
$first_type,
|
||||
$last_type,
|
||||
$from_docblock,
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($keyed_intersection_types['static'])) {
|
||||
unset($keyed_intersection_types['static']);
|
||||
$intersect_static = true;
|
||||
}
|
||||
if ($intersect_static
|
||||
&& $first_type instanceof TNamedObject
|
||||
) {
|
||||
$first_type->is_static = true;
|
||||
}
|
||||
|
||||
if (!$keyed_intersection_types && $intersect_static) {
|
||||
return new TNamedObject('static', false, false, [], $from_docblock);
|
||||
}
|
||||
|
||||
$first_type = array_shift($keyed_intersection_types);
|
||||
|
||||
if ($intersect_static
|
||||
&& $first_type instanceof TNamedObject
|
||||
) {
|
||||
$first_type->is_static = true;
|
||||
}
|
||||
|
||||
if ($keyed_intersection_types) {
|
||||
return $first_type->setIntersectionTypes($keyed_intersection_types);
|
||||
}
|
||||
if ($keyed_intersection_types) {
|
||||
/** @var non-empty-array<string,TIterable|TNamedObject|TCallableObject|TTemplateParam|TObjectWithProperties> $keyed_intersection_types */
|
||||
return $first_type->setIntersectionTypes($keyed_intersection_types);
|
||||
}
|
||||
|
||||
return $first_type;
|
||||
@@ -1397,6 +1355,16 @@ class TypeParser
|
||||
|
||||
$sealed = true;
|
||||
|
||||
$extra_params = null;
|
||||
|
||||
$last_property_branch = end($parse_tree->children);
|
||||
if ($last_property_branch instanceof GenericTree
|
||||
&& $last_property_branch->value === ''
|
||||
) {
|
||||
$extra_params = $last_property_branch->children;
|
||||
array_pop($parse_tree->children);
|
||||
}
|
||||
|
||||
foreach ($parse_tree->children as $i => $property_branch) {
|
||||
$class_string = false;
|
||||
|
||||
@@ -1517,14 +1485,249 @@ class TypeParser
|
||||
return new TArray([Type::getNever($from_docblock), Type::getNever($from_docblock)], $from_docblock);
|
||||
}
|
||||
|
||||
if ($extra_params) {
|
||||
if ($is_list && count($extra_params) !== 1) {
|
||||
throw new TypeParseTreeException('Must have exactly one extra field!');
|
||||
}
|
||||
if (!$is_list && count($extra_params) !== 2) {
|
||||
throw new TypeParseTreeException('Must have exactly two extra fields!');
|
||||
}
|
||||
$final_extra_params = $is_list ? [Type::getListKey(true)] : [];
|
||||
foreach ($extra_params as $child_tree) {
|
||||
$child_type = self::getTypeFromTree(
|
||||
$child_tree,
|
||||
$codebase,
|
||||
null,
|
||||
$template_type_map,
|
||||
$type_aliases,
|
||||
$from_docblock,
|
||||
);
|
||||
if ($child_type instanceof Atomic) {
|
||||
$child_type = new Union([$child_type]);
|
||||
}
|
||||
$final_extra_params []= $child_type;
|
||||
}
|
||||
$extra_params = $final_extra_params;
|
||||
}
|
||||
return new $class(
|
||||
$properties,
|
||||
$class_strings,
|
||||
$sealed
|
||||
$extra_params ?? ($sealed
|
||||
? null
|
||||
: [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()],
|
||||
: [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()]
|
||||
),
|
||||
$is_list,
|
||||
$from_docblock,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TNamedObject|TObjectWithProperties|TCallableObject|TIterable|TTemplateParam|TKeyedArray $intersection_type
|
||||
*/
|
||||
private static function extractIntersectionKey(Atomic $intersection_type): string
|
||||
{
|
||||
return $intersection_type instanceof TIterable || $intersection_type instanceof TKeyedArray
|
||||
? $intersection_type->getId()
|
||||
: $intersection_type->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-array<Atomic> $intersection_types
|
||||
* @return non-empty-array<string,TIterable|TNamedObject|TCallableObject|TTemplateParam|TObjectWithProperties|TKeyedArray>
|
||||
*/
|
||||
private static function extractKeyedIntersectionTypes(
|
||||
Codebase $codebase,
|
||||
array $intersection_types
|
||||
): array {
|
||||
$keyed_intersection_types = [];
|
||||
$callable_intersection = null;
|
||||
$any_object_type_found = $any_array_found = false;
|
||||
|
||||
$normalized_intersection_types = self::resolveTypeAliases(
|
||||
$codebase,
|
||||
$intersection_types,
|
||||
);
|
||||
|
||||
foreach ($normalized_intersection_types as $intersection_type) {
|
||||
if ($intersection_type instanceof TKeyedArray
|
||||
&& !$intersection_type instanceof TCallableKeyedArray
|
||||
) {
|
||||
$any_array_found = true;
|
||||
|
||||
if ($any_object_type_found) {
|
||||
throw new TypeParseTreeException(
|
||||
'The intersection type must not mix array and object types!',
|
||||
);
|
||||
}
|
||||
|
||||
$keyed_intersection_types[self::extractIntersectionKey($intersection_type)] = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$any_object_type_found = true;
|
||||
|
||||
if ($intersection_type instanceof TIterable
|
||||
|| $intersection_type instanceof TNamedObject
|
||||
|| $intersection_type instanceof TTemplateParam
|
||||
|| $intersection_type instanceof TObjectWithProperties
|
||||
) {
|
||||
$keyed_intersection_types[self::extractIntersectionKey($intersection_type)] = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (get_class($intersection_type) === TObject::class) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($intersection_type instanceof TCallable) {
|
||||
if ($callable_intersection !== null) {
|
||||
throw new TypeParseTreeException(
|
||||
'The intersection type must not contain more than one callable type!',
|
||||
);
|
||||
}
|
||||
$callable_intersection = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types must be all objects, '
|
||||
. get_class($intersection_type) . ' provided',
|
||||
);
|
||||
}
|
||||
|
||||
if ($callable_intersection !== null) {
|
||||
$callable_object_type = new TCallableObject(
|
||||
$callable_intersection->from_docblock,
|
||||
$callable_intersection,
|
||||
);
|
||||
|
||||
$keyed_intersection_types[self::extractIntersectionKey($callable_object_type)] = $callable_object_type;
|
||||
}
|
||||
|
||||
if ($any_object_type_found && $any_array_found) {
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types must be all objects or all keyed array.',
|
||||
);
|
||||
}
|
||||
|
||||
assert($keyed_intersection_types !== []);
|
||||
|
||||
return $keyed_intersection_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Atomic> $intersection_types
|
||||
* @return array<Atomic>
|
||||
*/
|
||||
private static function resolveTypeAliases(Codebase $codebase, array $intersection_types): array
|
||||
{
|
||||
$normalized_intersection_types = [];
|
||||
$modified = false;
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if (!$intersection_type instanceof TTypeAlias) {
|
||||
$normalized_intersection_types[] = [$intersection_type];
|
||||
continue;
|
||||
}
|
||||
|
||||
$modified = true;
|
||||
|
||||
$normalized_intersection_types[] = TypeExpander::expandAtomic(
|
||||
$codebase,
|
||||
$intersection_type,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if ($modified === false) {
|
||||
return $intersection_types;
|
||||
}
|
||||
|
||||
return self::resolveTypeAliases(
|
||||
$codebase,
|
||||
array_merge(...$normalized_intersection_types),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<TKeyedArray> $intersection_types
|
||||
* @param TKeyedArray|TArray $first_type
|
||||
* @param TKeyedArray|TArray $last_type
|
||||
*/
|
||||
private static function getTypeFromKeyedArrays(
|
||||
Codebase $codebase,
|
||||
array $intersection_types,
|
||||
Atomic $first_type,
|
||||
Atomic $last_type,
|
||||
bool $from_docblock
|
||||
): Atomic {
|
||||
/** @var non-empty-array<string|int, Union> */
|
||||
$properties = [];
|
||||
|
||||
if ($first_type instanceof TArray) {
|
||||
array_shift($intersection_types);
|
||||
} elseif ($last_type instanceof TArray) {
|
||||
array_pop($intersection_types);
|
||||
}
|
||||
|
||||
$all_sealed = true;
|
||||
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if ($intersection_type->fallback_params !== null) {
|
||||
$all_sealed = false;
|
||||
}
|
||||
|
||||
foreach ($intersection_type->properties as $property => $property_type) {
|
||||
if (!array_key_exists($property, $properties)) {
|
||||
$properties[$property] = $property_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_type = Type::intersectUnionTypes(
|
||||
$properties[$property],
|
||||
$property_type,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if ($new_type === null) {
|
||||
throw new TypeParseTreeException(
|
||||
'Incompatible intersection types for "' . $property . '", '
|
||||
. $properties[$property] . ' and ' . $property_type
|
||||
. ' provided',
|
||||
);
|
||||
}
|
||||
$properties[$property] = $new_type;
|
||||
}
|
||||
}
|
||||
|
||||
$first_or_last_type = $first_type instanceof TArray
|
||||
? $first_type
|
||||
: ($last_type instanceof TArray ? $last_type : null);
|
||||
|
||||
$fallback_params = null;
|
||||
|
||||
if ($first_or_last_type !== null) {
|
||||
$fallback_params = [
|
||||
$first_or_last_type->type_params[0],
|
||||
$first_or_last_type->type_params[1],
|
||||
];
|
||||
} elseif (!$all_sealed) {
|
||||
$fallback_params = [Type::getArrayKey(), Type::getMixed()];
|
||||
}
|
||||
|
||||
return new TKeyedArray(
|
||||
$properties,
|
||||
null,
|
||||
$fallback_params,
|
||||
false,
|
||||
$from_docblock,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user