mirror of
https://github.com/danog/psalm.git
synced 2024-12-02 17:52:45 +01:00
Merge branch 'list_bc_break' into tnon_empty_list_refactoring
This commit is contained in:
commit
4ceb56269e
@ -1,6 +1,12 @@
|
||||
# Upgrading from Psalm 4 to Psalm 5
|
||||
## Changed
|
||||
|
||||
- [BC] Psalm 5.1 will switch its internal representation of `list<T>` and `non-empty-list<T>` from the TList and TNonEmptyList classes to an unsealed list shape: the TList, TNonEmptyList and TCallableList classes will be removed.
|
||||
Nothing will change for users: `list<T>` and `non-empty-list<T>` syntax will remain supported and its semantics unchanged.
|
||||
Psalm 5.0 already deprecates the `TList`, `TNonEmptyList` and `TCallableList` classes: use `\Psalm\Type::getListAtomic`, `\Psalm\Type::getNonEmptyListAtomic` and `\Psalm\Type::getCallableListAtomic` to instantiate list atomics, or directly instantiate TKeyedArray objects with `is_list=true` where appropriate.
|
||||
|
||||
- [BC] The only optional boolean parameter of `TKeyedArray::getGenericArrayType` was removed, and will be replaced with a string parameter with a different meaning in Psalm 5.1.
|
||||
|
||||
- [BC] Shaped arrays can now be sealed: this brings many assertion improvements and bugfixes, see [the docs for more info](https://psalm.dev/docs/annotating_code/type_syntax/array_types/#sealed-object-like-arrays).
|
||||
|
||||
- [BC] All atomic types, `Psalm\Type\Union`, `Psalm\CodeLocation` and storages are fully immutable, use the new setter methods or the new constructors to change properties: these setter methods will return new instances without altering the original instance.
|
||||
|
@ -1808,9 +1808,9 @@ return [
|
||||
'DateTimeZone::__wakeup' => ['void'],
|
||||
'DateTimeZone::getLocation' => ['array|false'],
|
||||
'DateTimeZone::getName' => ['string'],
|
||||
'DateTimeZone::getOffset' => ['int|false', 'datetime'=>'DateTimeInterface'],
|
||||
'DateTimeZone::getOffset' => ['int', 'datetime'=>'DateTimeInterface'],
|
||||
'DateTimeZone::getTransitions' => ['list<array{ts: int, time: string, offset: int, isdst: bool, abbr: string}>|false', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'],
|
||||
'DateTimeZone::listAbbreviations' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>|false'],
|
||||
'DateTimeZone::listAbbreviations' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>'],
|
||||
'DateTimeZone::listIdentifiers' => ['list<string>', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'],
|
||||
'db2_autocommit' => ['mixed', 'connection'=>'resource', 'value='=>'int'],
|
||||
'db2_bind_param' => ['bool', 'stmt'=>'resource', 'parameter_number'=>'int', 'variable_name'=>'string', 'parameter_type='=>'int', 'data_type='=>'int', 'precision='=>'int', 'scale='=>'int'],
|
||||
@ -14675,12 +14675,12 @@ return [
|
||||
'time' => ['positive-int'],
|
||||
'time_nanosleep' => ['array{0:0|positive-int,1:0|positive-int}|bool', 'seconds'=>'positive-int', 'nanoseconds'=>'positive-int'],
|
||||
'time_sleep_until' => ['bool', 'timestamp'=>'float'],
|
||||
'timezone_abbreviations_list' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>|false'],
|
||||
'timezone_abbreviations_list' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>'],
|
||||
'timezone_identifiers_list' => ['list<string>', 'timezoneGroup='=>'int', 'countryCode='=>'?string'],
|
||||
'timezone_location_get' => ['array|false', 'object'=>'DateTimeZone'],
|
||||
'timezone_name_from_abbr' => ['string|false', 'abbr'=>'string', 'utcOffset='=>'int', 'isDST='=>'int'],
|
||||
'timezone_name_get' => ['string', 'object'=>'DateTimeZone'],
|
||||
'timezone_offset_get' => ['int|false', 'object'=>'DateTimeZone', 'datetime'=>'DateTimeInterface'],
|
||||
'timezone_offset_get' => ['int', 'object'=>'DateTimeZone', 'datetime'=>'DateTimeInterface'],
|
||||
'timezone_open' => ['DateTimeZone|false', 'timezone'=>'string'],
|
||||
'timezone_transitions_get' => ['list<array{ts: int, time: string, offset: int, isdst: bool, abbr: string}>|false', 'object'=>'DateTimeZone', 'timestampBegin='=>'int', 'timestampEnd='=>'int'],
|
||||
'timezone_version_get' => ['string'],
|
||||
|
@ -49,6 +49,10 @@ return [
|
||||
'old' => ['int|false'],
|
||||
'new' => ['int'],
|
||||
],
|
||||
'DateTimeZone::getOffset' => [
|
||||
'old' => ['int|false', 'datetime'=>'DateTimeInterface'],
|
||||
'new' => ['int', 'datetime'=>'DateTimeInterface'],
|
||||
],
|
||||
'DateTimeZone::listIdentifiers' => [
|
||||
'old' => ['list<string>|false', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'],
|
||||
'new' => ['list<string>', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'],
|
||||
@ -1421,6 +1425,10 @@ return [
|
||||
'old' => ['list<string>|false', 'timezoneGroup='=>'int', 'countryCode='=>'?string'],
|
||||
'new' => ['list<string>', 'timezoneGroup='=>'int', 'countryCode='=>'?string'],
|
||||
],
|
||||
'timezone_offset_get' => [
|
||||
'old' => ['int|false', 'object'=>'DateTimeZone', 'datetime'=>'DateTimeInterface'],
|
||||
'new' => ['int', 'object'=>'DateTimeZone', 'datetime'=>'DateTimeInterface'],
|
||||
],
|
||||
'xml_get_current_byte_index' => [
|
||||
'old' => ['int|false', 'parser'=>'resource'],
|
||||
'new' => ['int|false', 'parser'=>'XMLParser'],
|
||||
|
@ -1070,7 +1070,7 @@ return [
|
||||
'DateTimeZone::getName' => ['string'],
|
||||
'DateTimeZone::getOffset' => ['int|false', 'datetime'=>'DateTimeInterface'],
|
||||
'DateTimeZone::getTransitions' => ['list<array{ts: int, time: string, offset: int, isdst: bool, abbr: string}>|false', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'],
|
||||
'DateTimeZone::listAbbreviations' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>|false'],
|
||||
'DateTimeZone::listAbbreviations' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>'],
|
||||
'DateTimeZone::listIdentifiers' => ['list<string>|false', 'timezoneGroup='=>'int', 'countryCode='=>'string'],
|
||||
'Directory::close' => ['void', 'dir_handle='=>'resource'],
|
||||
'Directory::read' => ['string|false', 'dir_handle='=>'resource'],
|
||||
@ -15807,7 +15807,7 @@ return [
|
||||
'time' => ['positive-int'],
|
||||
'time_nanosleep' => ['array{0:0|positive-int,1:0|positive-int}|bool', 'seconds'=>'positive-int', 'nanoseconds'=>'positive-int'],
|
||||
'time_sleep_until' => ['bool', 'timestamp'=>'float'],
|
||||
'timezone_abbreviations_list' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>|false'],
|
||||
'timezone_abbreviations_list' => ['array<string, list<array{dst: bool, offset: int, timezone_id: string|null}>>'],
|
||||
'timezone_identifiers_list' => ['list<string>|false', 'timezoneGroup='=>'int', 'countryCode='=>'string'],
|
||||
'timezone_location_get' => ['array|false', 'object'=>'DateTimeZone'],
|
||||
'timezone_name_from_abbr' => ['string|false', 'abbr'=>'string', 'utcOffset='=>'int', 'isDST='=>'int'],
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="dev-master@53e3889745852409b704e0035d93e0819d522912">
|
||||
<files psalm-version="dev-master@12188caad4b2e4fd7a49a7199e97af2ba1d4c58e">
|
||||
<file src="examples/TemplateChecker.php">
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="2">
|
||||
<code>$comment_block->tags['variablesfrom'][0]</code>
|
||||
@ -72,6 +72,9 @@
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php">
|
||||
<DeprecatedClass occurrences="1">
|
||||
<code>new Atomic\TList(Type::getMixed())</code>
|
||||
</DeprecatedClass>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="28">
|
||||
<code>$assertion->rule[0]</code>
|
||||
<code>$assertion->rule[0]</code>
|
||||
@ -103,6 +106,13 @@
|
||||
<code>$gettype_expr->getArgs()[0]</code>
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php">
|
||||
<DeprecatedClass occurrences="3">
|
||||
<code>$array_atomic_type</code>
|
||||
<code>$array_atomic_type</code>
|
||||
<code>$array_atomic_type</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php">
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="2">
|
||||
<code>$invalid_left_messages[0]</code>
|
||||
@ -113,6 +123,12 @@
|
||||
<ComplexMethod occurrences="1">
|
||||
<code>verifyType</code>
|
||||
</ComplexMethod>
|
||||
<DeprecatedClass occurrences="4">
|
||||
<code>$array_type</code>
|
||||
<code>$unpacked_atomic_array</code>
|
||||
<code>TKeyedArray|TArray|TList|TClassStringMap</code>
|
||||
<code>TKeyedArray|TArray|TList|TClassStringMap|null</code>
|
||||
</DeprecatedClass>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="3">
|
||||
<code>$non_existent_method_ids[0]</code>
|
||||
<code>$parts[1]</code>
|
||||
@ -120,11 +136,21 @@
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php">
|
||||
<DeprecatedClass occurrences="2">
|
||||
<code>$array_type</code>
|
||||
<code>$array_type</code>
|
||||
</DeprecatedClass>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="1">
|
||||
<code>$arg_function_params[$argument_offset][0]</code>
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php">
|
||||
<DeprecatedClass occurrences="4">
|
||||
<code>$array_arg_type</code>
|
||||
<code>$array_type</code>
|
||||
<code>$array_type</code>
|
||||
<code>$replacement_array_type</code>
|
||||
</DeprecatedClass>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="4">
|
||||
<code>$args[0]</code>
|
||||
<code>$args[0]</code>
|
||||
@ -155,6 +181,14 @@
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php">
|
||||
<DeprecatedClass occurrences="6">
|
||||
<code>$array_type</code>
|
||||
<code>$array_type</code>
|
||||
<code>$type</code>
|
||||
<code>$type</code>
|
||||
<code>TArray|TKeyedArray|TList|TClassStringMap</code>
|
||||
<code>TList</code>
|
||||
</DeprecatedClass>
|
||||
<ReferenceConstraintViolation occurrences="3">
|
||||
<code>$stmt_type</code>
|
||||
<code>$stmt_type</code>
|
||||
@ -192,6 +226,9 @@
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Codebase/InternalCallMapHandler.php">
|
||||
<DeprecatedClass occurrences="1">
|
||||
<code>$array_atomic_type</code>
|
||||
</DeprecatedClass>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="2">
|
||||
<code>$callables[0]</code>
|
||||
<code>$callables[0]</code>
|
||||
@ -286,6 +323,9 @@
|
||||
</RedundantCondition>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php">
|
||||
<DeprecatedClass occurrences="1">
|
||||
<code>$array_type</code>
|
||||
</DeprecatedClass>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="1">
|
||||
<code>$flow_parts[0]</code>
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
@ -300,6 +340,33 @@
|
||||
<code>$cs[0]</code>
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php">
|
||||
<DeprecatedClass occurrences="1">
|
||||
<code>$array_arg_type</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Scanner/PhpStormMetaScanner.php">
|
||||
<DeprecatedClass occurrences="2">
|
||||
<code>$array_atomic_type</code>
|
||||
<code>$array_atomic_type</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php">
|
||||
<DeprecatedClass occurrences="6">
|
||||
<code>$container_type_part</code>
|
||||
<code>$container_type_part</code>
|
||||
<code>$input_type_part</code>
|
||||
<code>$input_type_part</code>
|
||||
<code>TArray|TKeyedArray|TList|TClassStringMap</code>
|
||||
<code>TArray|TKeyedArray|TList|TClassStringMap</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php">
|
||||
<DeprecatedClass occurrences="2">
|
||||
<code>TList::class</code>
|
||||
<code>TList::class</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php">
|
||||
<LessSpecificReturnStatement occurrences="1">
|
||||
<code>$callable</code>
|
||||
@ -308,6 +375,11 @@
|
||||
<code>TCallable|TClosure|null</code>
|
||||
</MoreSpecificReturnType>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Type/SimpleAssertionReconciler.php">
|
||||
<DeprecatedClass occurrences="3">
|
||||
<code>TList::class</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php">
|
||||
<ImpureMethodCall occurrences="2">
|
||||
<code>getClassTemplateTypes</code>
|
||||
@ -315,6 +387,7 @@
|
||||
</ImpureMethodCall>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Type/TypeCombiner.php">
|
||||
<DeprecatedClass occurrences="1"/>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="6">
|
||||
<code>$combination->array_type_params[1]</code>
|
||||
<code>$combination->array_type_params[1]</code>
|
||||
@ -349,6 +422,13 @@
|
||||
<code>traverse</code>
|
||||
</ImpureMethodCall>
|
||||
</file>
|
||||
<file src="src/Psalm/Type.php">
|
||||
<DeprecatedClass occurrences="3">
|
||||
<code>new TCallableList($of, null, null, $from_docblock)</code>
|
||||
<code>new TList($of, $from_docblock)</code>
|
||||
<code>new TNonEmptyList($of, null, null, $from_docblock)</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Type/Atomic.php">
|
||||
<ImpureMethodCall occurrences="12">
|
||||
<code>classExtendsOrImplements</code>
|
||||
@ -394,6 +474,11 @@
|
||||
<code>getMostSpecificTypeFromBounds</code>
|
||||
</ImpureMethodCall>
|
||||
</file>
|
||||
<file src="src/Psalm/Type/Atomic/TCallableList.php">
|
||||
<DeprecatedClass occurrences="1">
|
||||
<code>TNonEmptyList</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Type/Atomic/TClassString.php">
|
||||
<ImpureMethodCall occurrences="1">
|
||||
<code>replace</code>
|
||||
@ -441,6 +526,11 @@
|
||||
<code>$cloned->type_param</code>
|
||||
</ImpurePropertyAssignment>
|
||||
</file>
|
||||
<file src="src/Psalm/Type/Atomic/TNonEmptyList.php">
|
||||
<DeprecatedClass occurrences="1">
|
||||
<code>TList</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="src/Psalm/Type/Atomic/TObjectWithProperties.php">
|
||||
<ImpureMethodCall occurrences="2">
|
||||
<code>replace</code>
|
||||
@ -506,6 +596,11 @@
|
||||
<code>allFloatLiterals</code>
|
||||
</PossiblyUnusedMethod>
|
||||
</file>
|
||||
<file src="tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php">
|
||||
<DeprecatedClass occurrences="1">
|
||||
<code>new Type\Atomic\TList($last_callable_arg->return_type ?? Type::getMixed())</code>
|
||||
</DeprecatedClass>
|
||||
</file>
|
||||
<file src="tests/Internal/Codebase/InternalCallMapHandlerTest.php">
|
||||
<UnusedPsalmSuppress occurrences="1">
|
||||
<code>UndefinedMethod</code>
|
||||
|
@ -63,9 +63,12 @@ use function count;
|
||||
use function dirname;
|
||||
use function explode;
|
||||
use function extension_loaded;
|
||||
use function fclose;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function filetype;
|
||||
use function flock;
|
||||
use function fopen;
|
||||
use function get_class;
|
||||
use function get_defined_constants;
|
||||
use function get_defined_functions;
|
||||
@ -77,6 +80,7 @@ use function is_a;
|
||||
use function is_array;
|
||||
use function is_dir;
|
||||
use function is_file;
|
||||
use function is_resource;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use function libxml_clear_errors;
|
||||
@ -103,6 +107,7 @@ use function substr;
|
||||
use function substr_count;
|
||||
use function sys_get_temp_dir;
|
||||
use function unlink;
|
||||
use function usleep;
|
||||
use function version_compare;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
@ -112,6 +117,7 @@ use const LIBXML_ERR_ERROR;
|
||||
use const LIBXML_ERR_FATAL;
|
||||
use const LIBXML_NONET;
|
||||
use const LIBXML_NOWARNING;
|
||||
use const LOCK_EX;
|
||||
use const PHP_EOL;
|
||||
use const PHP_VERSION_ID;
|
||||
use const PSALM_VERSION;
|
||||
@ -2403,9 +2409,33 @@ class Config
|
||||
if (filetype($full_path) === 'dir') {
|
||||
self::removeCacheDirectory($full_path);
|
||||
} else {
|
||||
$fp = fopen($full_path, 'c');
|
||||
if ($fp === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$max_wait_cycles = 5;
|
||||
$has_lock = false;
|
||||
while ($max_wait_cycles > 0) {
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
$has_lock = true;
|
||||
break;
|
||||
}
|
||||
$max_wait_cycles--;
|
||||
usleep(50_000);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$has_lock) {
|
||||
throw new RuntimeException('Could not acquire lock for deletion of ' . $full_path);
|
||||
}
|
||||
|
||||
unlink($full_path);
|
||||
fclose($fp);
|
||||
} catch (RuntimeException $e) {
|
||||
if (is_resource($fp)) {
|
||||
fclose($fp);
|
||||
}
|
||||
clearstatcache(true, $full_path);
|
||||
if (file_exists($full_path)) {
|
||||
// rethrow the error with default message
|
||||
|
@ -64,6 +64,7 @@ use function array_keys;
|
||||
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;
|
||||
@ -348,37 +349,46 @@ class ProjectAnalyzer
|
||||
|
||||
private function clearCacheDirectoryIfConfigOrComposerLockfileChanged(): void
|
||||
{
|
||||
$cache_directory = $this->config->getCacheDirectory();
|
||||
if ($cache_directory === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->project_cache_provider
|
||||
&& $this->project_cache_provider->hasLockfileChanged()
|
||||
) {
|
||||
$this->progress->debug(
|
||||
'Composer lockfile change detected, clearing cache' . "\n"
|
||||
);
|
||||
// we only clear the cache if it actually exists
|
||||
// if it's not populated yet, we don't clear anything but populate the cache instead
|
||||
clearstatcache(true, $cache_directory);
|
||||
if (is_dir($cache_directory)) {
|
||||
$this->progress->debug(
|
||||
'Composer lockfile change detected, clearing cache directory ' . $cache_directory . "\n"
|
||||
);
|
||||
|
||||
$cache_directory = $this->config->getCacheDirectory();
|
||||
if ($cache_directory !== null) {
|
||||
Config::removeCacheDirectory($cache_directory);
|
||||
}
|
||||
|
||||
if ($this->file_reference_provider->cache) {
|
||||
$this->file_reference_provider->cache->hasConfigChanged();
|
||||
$this->file_reference_provider->cache->setConfigHashCache();
|
||||
}
|
||||
|
||||
$this->project_cache_provider->updateComposerLockHash();
|
||||
} elseif ($this->file_reference_provider->cache
|
||||
&& $this->file_reference_provider->cache->hasConfigChanged()
|
||||
) {
|
||||
$this->progress->debug(
|
||||
'Config change detected, clearing cache' . "\n"
|
||||
);
|
||||
clearstatcache(true, $cache_directory);
|
||||
if (is_dir($cache_directory)) {
|
||||
$this->progress->debug(
|
||||
'Config change detected, clearing cache directory ' . $cache_directory . "\n"
|
||||
);
|
||||
|
||||
$cache_directory = $this->config->getCacheDirectory();
|
||||
if ($cache_directory !== null) {
|
||||
Config::removeCacheDirectory($cache_directory);
|
||||
}
|
||||
|
||||
$this->file_reference_provider->cache->setConfigHashCache();
|
||||
|
||||
if ($this->project_cache_provider) {
|
||||
$this->project_cache_provider->hasLockfileChanged();
|
||||
$this->project_cache_provider->updateComposerLockHash();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,12 +147,7 @@ class ArrayAnalyzer
|
||||
if ($array_creation_info->can_be_empty) {
|
||||
$array_type = Type::getListAtomic($item_value_type ?? Type::getMixed());
|
||||
} else {
|
||||
$array_type = new TKeyedArray(
|
||||
[$item_value_type ?? Type::getMixed()],
|
||||
null,
|
||||
[Type::getInt(), Type::getMixed()],
|
||||
true
|
||||
);
|
||||
$array_type = Type::getNonEmptyListAtomic($item_value_type ?? Type::getMixed());
|
||||
}
|
||||
|
||||
$stmt_type = new Union([
|
||||
|
@ -849,7 +849,9 @@ class NewAnalyzer extends CallAnalyzer
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($lhs_type_part instanceof TMixed) {
|
||||
} elseif ($lhs_type_part instanceof TMixed
|
||||
|| $lhs_type_part instanceof TObject
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MixedMethodCall(
|
||||
'Cannot call constructor on an unknown class',
|
||||
@ -865,6 +867,9 @@ class NewAnalyzer extends CallAnalyzer
|
||||
&& $stmt_class_type->ignore_nullable_issues
|
||||
) {
|
||||
// do nothing
|
||||
} elseif ($lhs_type_part instanceof TNamedObject) {
|
||||
$new_type = Type::combineUnionTypes($new_type, new Union([$lhs_type_part]));
|
||||
continue;
|
||||
} elseif (IssueBuffer::accepts(
|
||||
new UndefinedClass(
|
||||
'Type ' . $lhs_type_part . ' cannot be called as a class',
|
||||
|
@ -62,7 +62,7 @@ final class LanguageServer
|
||||
public static function run(array $argv): void
|
||||
{
|
||||
gc_disable();
|
||||
ErrorHandler::install();
|
||||
ErrorHandler::install($argv);
|
||||
$valid_short_options = [
|
||||
'h',
|
||||
'v',
|
||||
|
@ -168,7 +168,7 @@ final class Psalm
|
||||
gc_collect_cycles();
|
||||
gc_disable();
|
||||
|
||||
ErrorHandler::install();
|
||||
ErrorHandler::install($argv);
|
||||
|
||||
$args = array_slice($argv, 1);
|
||||
|
||||
@ -191,7 +191,6 @@ final class Psalm
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
if (array_key_exists('h', $options)) {
|
||||
echo self::getHelpText();
|
||||
/*
|
||||
|
@ -21,6 +21,7 @@ use Psalm\Internal\Scanner\ParsedDocblock;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Progress\DebugProgress;
|
||||
use Psalm\Progress\DefaultProgress;
|
||||
use Psalm\Progress\VoidProgress;
|
||||
use Psalm\Report;
|
||||
use Psalm\Report\ReportOptions;
|
||||
|
||||
@ -86,7 +87,8 @@ final class Psalter
|
||||
'find-unused-code', 'threads:', 'codeowner:',
|
||||
'allow-backwards-incompatible-changes:',
|
||||
'add-newline-between-docblock-annotations:',
|
||||
'no-cache'
|
||||
'no-cache',
|
||||
'no-progress'
|
||||
];
|
||||
|
||||
/** @param array<int,string> $argv */
|
||||
@ -95,7 +97,7 @@ final class Psalter
|
||||
gc_collect_cycles();
|
||||
gc_disable();
|
||||
|
||||
ErrorHandler::install();
|
||||
ErrorHandler::install($argv);
|
||||
|
||||
self::setMemoryLimit();
|
||||
|
||||
@ -130,6 +132,9 @@ final class Psalter
|
||||
-m, --monochrome
|
||||
Enable monochrome output
|
||||
|
||||
--no-progress
|
||||
Disable the progress indicator
|
||||
|
||||
-r, --root
|
||||
If running Psalm globally you'll need to specify a project root. Defaults to cwd
|
||||
|
||||
@ -257,9 +262,13 @@ final class Psalter
|
||||
}
|
||||
|
||||
$debug = array_key_exists('debug', $options) || array_key_exists('debug-by-line', $options);
|
||||
$progress = $debug
|
||||
? new DebugProgress()
|
||||
: new DefaultProgress();
|
||||
if ($debug) {
|
||||
$progress = new DebugProgress();
|
||||
} elseif (isset($options['no-progress'])) {
|
||||
$progress = new VoidProgress();
|
||||
} else {
|
||||
$progress = new DefaultProgress();
|
||||
}
|
||||
|
||||
$stdout_report_options = new ReportOptions();
|
||||
$stdout_report_options->use_color = !array_key_exists('m', $options);
|
||||
|
@ -72,7 +72,7 @@ final class Refactor
|
||||
gc_collect_cycles();
|
||||
gc_disable();
|
||||
|
||||
ErrorHandler::install();
|
||||
ErrorHandler::install($argv);
|
||||
|
||||
$args = array_slice($argv, 1);
|
||||
|
||||
|
@ -8,6 +8,7 @@ use Throwable;
|
||||
use function defined;
|
||||
use function error_reporting;
|
||||
use function fwrite;
|
||||
use function implode;
|
||||
use function ini_set;
|
||||
use function set_error_handler;
|
||||
use function set_exception_handler;
|
||||
@ -24,8 +25,15 @@ final class ErrorHandler
|
||||
/** @var bool */
|
||||
private static $exceptions_enabled = true;
|
||||
|
||||
public static function install(): void
|
||||
/** @var string */
|
||||
private static $args = '';
|
||||
|
||||
/**
|
||||
* @param array<int,string> $argv
|
||||
*/
|
||||
public static function install(array $argv = array()): void
|
||||
{
|
||||
self::$args = implode(' ', $argv);
|
||||
self::setErrorReporting();
|
||||
self::installErrorHandler();
|
||||
self::installExceptionHandler();
|
||||
@ -67,7 +75,9 @@ final class ErrorHandler
|
||||
): bool {
|
||||
if (ErrorHandler::$exceptions_enabled && ($error_code & error_reporting())) {
|
||||
throw new RuntimeException(
|
||||
'PHP Error: ' . $error_message . ' in ' . $error_filename . ':' . $error_line,
|
||||
'PHP Error: ' . $error_message
|
||||
. ' in ' . $error_filename . ':' . $error_line
|
||||
. ' for command with CLI args "' . ErrorHandler::$args . '"',
|
||||
$error_code
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ use Psalm\Internal\Scanner\ParsedDocblock;
|
||||
use Psalm\Issue\InvalidDocblock;
|
||||
use Psalm\IssueBuffer;
|
||||
|
||||
use function array_keys;
|
||||
use function array_shift;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
@ -704,17 +705,44 @@ class FunctionLikeDocblockParser
|
||||
PhpParser\Comment\Doc $comment
|
||||
): void {
|
||||
if (isset($parsed_docblock->tags['psalm-import-type'])) {
|
||||
foreach ($parsed_docblock->tags['psalm-import-type'] as $offset => $_) {
|
||||
$info->unexpected_tags['psalm-import-type']['lines'][] = self::docblockLineNumber($comment, $offset);
|
||||
}
|
||||
$info->unexpected_tags['psalm-import-type']['lines'] = self::tagOffsetsToLines(
|
||||
array_keys($parsed_docblock->tags['psalm-import-type']),
|
||||
$comment
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->combined_tags['var'])) {
|
||||
$info->unexpected_tags['var'] = ['lines' => [], 'suggested_replacement' => 'param'];
|
||||
foreach ($parsed_docblock->combined_tags['var'] as $offset => $_) {
|
||||
$info->unexpected_tags['var']['lines'][] = self::docblockLineNumber($comment, $offset);
|
||||
}
|
||||
$info->unexpected_tags['var'] = [
|
||||
'lines' => self::tagOffsetsToLines(
|
||||
array_keys($parsed_docblock->combined_tags['var']),
|
||||
$comment
|
||||
),
|
||||
'suggested_replacement' => 'param'
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-consistent-constructor'])) {
|
||||
$info->unexpected_tags['psalm-consistent-constructor'] = [
|
||||
'lines' => self::tagOffsetsToLines(
|
||||
array_keys($parsed_docblock->tags['psalm-consistent-constructor']),
|
||||
$comment
|
||||
),
|
||||
'suggested_replacement' => 'psalm-consistent-constructor on a class level',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $offsets
|
||||
* @return list<int>
|
||||
*/
|
||||
private static function tagOffsetsToLines(array $offsets, PhpParser\Comment\Doc $comment): array
|
||||
{
|
||||
$ret = [];
|
||||
foreach ($offsets as $offset) {
|
||||
$ret[] = self::docblockLineNumber($comment, $offset);
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private static function docblockLineNumber(PhpParser\Comment\Doc $comment, int $offset): int
|
||||
|
@ -615,7 +615,8 @@ class FunctionLikeDocblockScanner
|
||||
true
|
||||
),
|
||||
null,
|
||||
$template_types
|
||||
$template_types,
|
||||
$type_aliases
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
|
@ -32,6 +32,7 @@ use Psalm\Storage\MethodStorage;
|
||||
use Psalm\Type;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function end;
|
||||
use function explode;
|
||||
@ -187,7 +188,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements FileSour
|
||||
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
|
||||
}
|
||||
|
||||
$this->type_aliases += $classlike_node_scanner->type_aliases;
|
||||
$this->type_aliases = array_merge($this->type_aliases, $classlike_node_scanner->type_aliases);
|
||||
} elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) {
|
||||
foreach ($node->catches as $catch) {
|
||||
foreach ($catch->types as $catch_type) {
|
||||
|
@ -66,9 +66,7 @@ class FileReferenceCacheProvider
|
||||
public function hasConfigChanged(): bool
|
||||
{
|
||||
$new_hash = $this->config->computeHash();
|
||||
$has_changed = $new_hash !== $this->getConfigHashCache();
|
||||
$this->setConfigHashCache($new_hash);
|
||||
return $has_changed;
|
||||
return $new_hash !== $this->getConfigHashCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -998,7 +996,7 @@ class FileReferenceCacheProvider
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setConfigHashCache(string $hash): void
|
||||
public function setConfigHashCache(string $hash = ''): void
|
||||
{
|
||||
$cache_directory = Config::getInstance()->getCacheDirectory();
|
||||
|
||||
@ -1006,6 +1004,10 @@ class FileReferenceCacheProvider
|
||||
return;
|
||||
}
|
||||
|
||||
if ($hash === '') {
|
||||
$hash = $this->config->computeHash();
|
||||
}
|
||||
|
||||
if (!is_dir($cache_directory)) {
|
||||
try {
|
||||
if (mkdir($cache_directory, 0777, true) === false) {
|
||||
|
@ -87,7 +87,10 @@ class ArrayTypeComparator
|
||||
&& $input_type_part instanceof TKeyedArray
|
||||
) {
|
||||
if ($input_type_part->is_list) {
|
||||
$input_type_part = $input_type_part->getList();
|
||||
/** @var TList|TNonEmptyList */
|
||||
$input_type_part = $input_type_part->isNonEmpty()
|
||||
? Type::getNonEmptyListAtomic($input_type_part->getGenericValueType())
|
||||
: Type::getListAtomic($input_type_part->getGenericValueType());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@ -145,7 +148,10 @@ class ArrayTypeComparator
|
||||
|
||||
if ($container_type_part instanceof TKeyedArray) {
|
||||
if ($container_type_part->is_list) {
|
||||
$container_type_part = $container_type_part->getList();
|
||||
/** @var TList|TNonEmptyList */
|
||||
$container_type_part = $container_type_part->isNonEmpty()
|
||||
? Type::getNonEmptyListAtomic($container_type_part->getGenericValueType())
|
||||
: Type::getListAtomic($container_type_part->getGenericValueType());
|
||||
|
||||
return self::isContainedBy(
|
||||
$codebase,
|
||||
|
@ -690,7 +690,7 @@ class TypeParser
|
||||
}
|
||||
|
||||
if ($generic_type_value === 'non-empty-list') {
|
||||
return Type::getNonEmptyListAtomic($generic_params[0], null, null, $from_docblock);
|
||||
return Type::getNonEmptyListAtomic($generic_params[0], $from_docblock);
|
||||
}
|
||||
|
||||
if ($generic_type_value === 'class-string'
|
||||
|
@ -14,6 +14,7 @@ use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TArrayKey;
|
||||
use Psalm\Type\Atomic\TBool;
|
||||
use Psalm\Type\Atomic\TCallableList;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
@ -441,28 +442,26 @@ abstract class Type
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getList(?Union $of = null): Union
|
||||
public static function getList(?Union $of = null, bool $from_docblock = false): Union
|
||||
{
|
||||
return new Union([self::getListAtomic($of)]);
|
||||
return new Union([self::getListAtomic($of ?? self::getMixed($from_docblock), $from_docblock)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getNonEmptyList(?Union $of = null): Union
|
||||
public static function getNonEmptyList(?Union $of = null, bool $from_docblock = false): Union
|
||||
{
|
||||
return new Union([self::getNonEmptyListAtomic($of)]);
|
||||
return new Union([self::getNonEmptyListAtomic($of ?? self::getMixed($from_docblock), $from_docblock)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getListAtomic(?Union $of = null): TKeyedArray
|
||||
public static function getListAtomic(Union $of, bool $from_docblock = false): Atomic
|
||||
{
|
||||
return new TKeyedArray(
|
||||
[$of !== null
|
||||
? $of->setPossiblyUndefined(true)
|
||||
: new Union([new TMixed()], ['possibly_undefined' => true])],
|
||||
[$of->setPossiblyUndefined(true)],
|
||||
null,
|
||||
[self::getInt(), $of],
|
||||
true
|
||||
@ -472,18 +471,32 @@ abstract class Type
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getNonEmptyListAtomic(?Union $of = null): TKeyedArray
|
||||
public static function getNonEmptyListAtomic(Union $of, bool $from_docblock = false): Atomic
|
||||
{
|
||||
return new TKeyedArray(
|
||||
[$of !== null
|
||||
? $of->setPossiblyUndefined(false)
|
||||
: new Union([new TMixed()], ['possibly_undefined' => false])],
|
||||
[$of->setPossiblyUndefined(false)],
|
||||
null,
|
||||
[self::getInt(), $of],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getCallableListAtomic(Union $of, bool $from_docblock = false): Atomic
|
||||
{
|
||||
// The following code will be uncommented in Psalm 5.1
|
||||
//$of = $of->setPossiblyUndefined(false);
|
||||
//return new TCallableKeyedArray(
|
||||
// [$of, $of],
|
||||
// null,
|
||||
// [self::getInt(), $of],
|
||||
// true
|
||||
//);
|
||||
return new TCallableList($of, null, null, $from_docblock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
|
@ -350,7 +350,7 @@ class TKeyedArray extends Atomic
|
||||
|
||||
$value_type = $value_type->setPossiblyUndefined(false);
|
||||
|
||||
if ($has_defined_keys) {
|
||||
if ($has_defined_keys || $this->fallback_params !== null) {
|
||||
return new TNonEmptyArray([$key_type, $value_type]);
|
||||
}
|
||||
return new TArray([$key_type, $value_type]);
|
||||
|
@ -795,6 +795,53 @@ class ClassTest extends TestCase
|
||||
abstract class C implements I {}
|
||||
',
|
||||
],
|
||||
'newOnNamedObject' => [
|
||||
'code' => '<?php
|
||||
$o = new stdClass;
|
||||
$o2 = new $o;
|
||||
',
|
||||
'assertions' => [
|
||||
'$o2===' => 'stdClass',
|
||||
],
|
||||
],
|
||||
'newOnObjectOfAnonymousClass' => [
|
||||
'code' => '<?php
|
||||
function f(): object {
|
||||
$o = new class {};
|
||||
return new $o;
|
||||
}
|
||||
',
|
||||
],
|
||||
'newOnObjectOfAnonymousExtendingNamed' => [
|
||||
'code' => '<?php
|
||||
function f(): Exception {
|
||||
$o = new class extends Exception {};
|
||||
return new $o;
|
||||
}
|
||||
',
|
||||
],
|
||||
'newOnObjectOfAnonymousClassImplementingNamed' => [
|
||||
'code' => '<?php
|
||||
interface I {}
|
||||
function f(): I {
|
||||
$o = new class implements I {};
|
||||
return new $o;
|
||||
}
|
||||
',
|
||||
],
|
||||
'throwAnonymousObjects' => [
|
||||
'code' => '<?php
|
||||
throw new class extends Exception {};
|
||||
',
|
||||
],
|
||||
'throwTheResultOfNewOnAnAnonymousClass' => [
|
||||
'code' => '<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$test = new class extends \Exception { };
|
||||
throw new $test();
|
||||
'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@ -1151,7 +1198,16 @@ class ClassTest extends TestCase
|
||||
class Bar {}
|
||||
',
|
||||
'error_message' => 'ReservedWord',
|
||||
]
|
||||
],
|
||||
'newOnObject' => [
|
||||
'code' => '<?php
|
||||
function f(object $o): object
|
||||
{
|
||||
return new $o;
|
||||
}
|
||||
',
|
||||
'error_message' => 'MixedMethodCall',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +135,7 @@ class FunctionLikeDocblockParserTest extends BaseTestCase
|
||||
$doc = '/**
|
||||
* @psalm-import-type abcd
|
||||
* @var int $p
|
||||
* @psalm-consistent-constructor
|
||||
*/
|
||||
';
|
||||
$php_parser_doc = new Doc($doc, 0);
|
||||
@ -147,6 +148,10 @@ class FunctionLikeDocblockParserTest extends BaseTestCase
|
||||
[
|
||||
'psalm-import-type' => ['lines' => [1]],
|
||||
'var' => ['lines' => [2], 'suggested_replacement' => 'param'],
|
||||
'psalm-consistent-constructor' => [
|
||||
'lines' => [3],
|
||||
'suggested_replacement' => 'psalm-consistent-constructor on a class level'
|
||||
]
|
||||
],
|
||||
$function_docblock->unexpected_tags
|
||||
);
|
||||
|
@ -430,8 +430,6 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
'stream_set_chunk_size',
|
||||
'substr',
|
||||
'substr_compare',
|
||||
'timezone_abbreviations_list',
|
||||
'timezone_offset_get',
|
||||
'user_error',
|
||||
'xml_get_current_byte_index',
|
||||
'xml_get_current_column_number',
|
||||
|
@ -593,6 +593,69 @@ class TypeAnnotationTest extends TestCase
|
||||
'$output' => 'string',
|
||||
],
|
||||
],
|
||||
'importedTypeUsedInAssertion' => [
|
||||
'code' => '<?php
|
||||
/** @psalm-type Foo = string */
|
||||
class A {}
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
* @psalm-import-type Foo from A as FooAlias
|
||||
*/
|
||||
class B {
|
||||
/**
|
||||
* @param mixed $input
|
||||
* @psalm-return FooAlias
|
||||
*/
|
||||
public function convertToFoo($input) {
|
||||
$this->assertFoo($input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @psalm-assert FooAlias $value
|
||||
*/
|
||||
private function assertFoo($value): void {
|
||||
if(!is_string($value)) {
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$instance = new B();
|
||||
$output = $instance->convertToFoo("hallo");
|
||||
',
|
||||
'assertions' => [
|
||||
'$output' => 'string',
|
||||
]
|
||||
],
|
||||
'importedTypeUsedInOtherType' => [
|
||||
'code' => '<?php
|
||||
/** @psalm-type OpeningTypes=self::TYPE_A|self::TYPE_B */
|
||||
class Foo {
|
||||
public const TYPE_A = 1;
|
||||
public const TYPE_B = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-import-type OpeningTypes from Foo
|
||||
* @psalm-type OpeningTypeAssignment=list<OpeningTypes>
|
||||
*/
|
||||
class Main {
|
||||
/** @return OpeningTypeAssignment */
|
||||
public function doStuff(): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$instance = new Main();
|
||||
$output = $instance->doStuff();
|
||||
',
|
||||
'assertions' => [
|
||||
'$output===' => 'list<1|2>',
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user