Composer updates

This commit is contained in:
Clemens Schwaighofer
2023-05-29 16:21:31 +09:00
parent 250067927a
commit 7b5ad92e66
304 changed files with 11398 additions and 3199 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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',
];
/**

View File

@@ -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

View File

@@ -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]);

View File

@@ -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

View File

@@ -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');
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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('Shouldnt get a generic param here');
}

View File

@@ -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,
);

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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

View File

@@ -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,
),

View File

@@ -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;
}

View File

@@ -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']))
) {

View File

@@ -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();
}
}
}

View File

@@ -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)),
);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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,
);
}
}
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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()),
],
];
}
}

View File

@@ -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('Shouldnt get a generic param here');
}

View File

@@ -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(

View File

@@ -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();

View File

@@ -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;

View File

@@ -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());

View File

@@ -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(

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -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);
}
}

View File

@@ -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).

View File

@@ -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
{

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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]));
}

View 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;
}
}

View File

@@ -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) {

View File

@@ -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,
];
}
}

View File

@@ -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',
];
}

View File

@@ -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);
},
);
}
}

View 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,
],
],
]);
}
}

View 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;
}
}

View File

@@ -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

View File

@@ -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 servers 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 doesnt specify the payload since no
* interpretation of the data happens in the protocol.
* Most clients even dont 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

View File

@@ -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;

View 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;
}
}

View 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));
}
}
}

View File

@@ -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;

View 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;
}
}

View 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
{
}
}

View 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;
}
}

View 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
{
}
}

View 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
{
}
}

View 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;
}
}

View File

@@ -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)) {

View File

@@ -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);
}
}

View File

@@ -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');

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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);
}
}
}
}

View File

@@ -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]);
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);
}
/**

View File

@@ -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()]);
}
}

View 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;
}
}

View 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()]);
}
}

View File

@@ -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;

View File

@@ -0,0 +1,11 @@
<?php
namespace Psalm\Internal\Scanner\UnresolvedConstant;
/**
* @psalm-immutable
* @internal
*/
class EnumNameFetch extends EnumPropertyFetch
{
}

View 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;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Psalm\Internal\Scanner\UnresolvedConstant;
/**
* @psalm-immutable
* @internal
*/
class EnumValueFetch extends EnumPropertyFetch
{
}

View File

@@ -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,
);
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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 = [];

View File

@@ -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;

View File

@@ -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());
}
}

View File

@@ -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