1
0
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:
Daniil Gentili 2022-11-26 13:17:58 +01:00
commit 4ceb56269e
26 changed files with 409 additions and 69 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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-&gt;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-&gt;rule[0]</code>
<code>$assertion-&gt;rule[0]</code>
@ -103,6 +106,13 @@
<code>$gettype_expr-&gt;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-&gt;array_type_params[1]</code>
<code>$combination-&gt;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-&gt;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-&gt;return_type ?? Type::getMixed())</code>
</DeprecatedClass>
</file>
<file src="tests/Internal/Codebase/InternalCallMapHandlerTest.php">
<UnusedPsalmSuppress occurrences="1">
<code>UndefinedMethod</code>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,7 +72,7 @@ final class Refactor
gc_collect_cycles();
gc_disable();
ErrorHandler::install();
ErrorHandler::install($argv);
$args = array_slice($argv, 1);

View File

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

View File

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

View File

@ -615,7 +615,8 @@ class FunctionLikeDocblockScanner
true
),
null,
$template_types
$template_types,
$type_aliases
);
} catch (TypeParseTreeException $e) {
$storage->docblock_issues[] = new InvalidDocblock(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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