diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c4d5026289..341a05bb9ef 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,18 +2,15 @@ # See https://app.circleci.com/pipelines/github/vimeo/psalm version: 2.1 executors: - php-74: - docker: - - image: thecodingmachine/php:7.4-v4-cli - php-80: - docker: - - image: thecodingmachine/php:8.0-v4-cli php-81: docker: - image: thecodingmachine/php:8.1-v4-cli + php-82: + docker: + - image: thecodingmachine/php:8.2-v4-cli jobs: "Code Style Analysis": - executor: php-74 + executor: php-81 steps: - checkout @@ -41,7 +38,7 @@ jobs: command: vendor/bin/phpcs -d memory_limit=512M phar-build: - executor: php-74 + executor: php-81 steps: - attach_workspace: at: /home/docker/project/ diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index 8d75dfe63c3..ba0c5c6d1e9 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -38,7 +38,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' tools: composer:v2 coverage: none env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf9fbf7b147..33f2a6b945b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' tools: composer:v2 coverage: none env: @@ -57,7 +57,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' tools: composer:v2 coverage: none env: @@ -125,7 +125,6 @@ jobs: fail-fast: false matrix: php-version: - - "8.0" - "8.1" - "8.2" - "8.3" diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 5b6a06c4e08..3d89ba846af 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -54,7 +54,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.1' ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M tools: composer:v2 coverage: none diff --git a/UPGRADING.md b/UPGRADING.md index 01f1a67ddda..2fb773dc2df 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,8 @@ # Upgrading from Psalm 5 to Psalm 6 ## Changed +- The minimum PHP version was raised to PHP 8.1.17. + - [BC] Switched the internal representation of `list` and `non-empty-list` from the TList and TNonEmptyList classes to an unsealed list shape: the TList, TNonEmptyList and TCallableList classes were removed. Nothing will change for users: the `list` and `non-empty-list` syntax will remain supported and its semantics unchanged. Psalm 5 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. @@ -9,6 +11,8 @@ - [BC] The `TDependentListKey` type was removed and replaced with an optional property of the `TIntRange` type. +- [BC] The return type of `Psalm\Internal\LanguageServer\ProtocolWriter#write() changed from `Amp\Promise` to `void` due to the switch to Amp v3 + # Upgrading from Psalm 4 to Psalm 5 ## Changed diff --git a/composer.json b/composer.json index 7df7c97af38..6c5c76f8eb5 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.1.17 || ~8.2.4 || ~8.3.0", "ext-SimpleXML": "*", "ext-ctype": "*", "ext-dom": "*", @@ -24,8 +24,8 @@ "ext-mbstring": "*", "ext-tokenizer": "*", "composer-runtime-api": "^2", - "amphp/amp": "^2.4.2", - "amphp/byte-stream": "^1.5", + "amphp/amp": "^3", + "amphp/byte-stream": "^2", "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", @@ -44,7 +44,7 @@ }, "require-dev": { "ext-curl": "*", - "amphp/phpunit-util": "^2.0", + "amphp/phpunit-util": "^3", "bamarni/composer-bin-plugin": "^1.4", "brianium/paratest": "^6.9", "mockery/mockery": "^1.5", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 69c51858103..b299a6eedbe 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -290,6 +290,19 @@ props[0]]]> + + + $config + + + [$config] + + + + + $result + + $method_id_parts[1] diff --git a/src/Psalm/Internal/Algebra/FormulaGenerator.php b/src/Psalm/Internal/Algebra/FormulaGenerator.php index c5d75f8829c..9c5482c4a57 100644 --- a/src/Psalm/Internal/Algebra/FormulaGenerator.php +++ b/src/Psalm/Internal/Algebra/FormulaGenerator.php @@ -149,7 +149,6 @@ public static function getFormula( $redefined = false; if ($var[0] === '=') { - /** @var string */ $var = substr($var, 1); $redefined = true; } @@ -420,7 +419,6 @@ public static function getFormula( $redefined = false; if ($var[0] === '=') { - /** @var string */ $var = substr($var, 1); $redefined = true; } diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index a01318a3e81..19f2b640749 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -14,7 +14,6 @@ use function assert; use function count; -use function is_string; use function preg_replace; use function strpos; use function strtolower; @@ -244,7 +243,7 @@ public static function getIdentifierParts(string $identifier): array while (($pos = strpos($identifier, "\\")) !== false) { if ($pos > 0) { $part = substr($identifier, 0, $pos); - assert(is_string($part) && $part !== ""); + assert($part !== ""); $parts[] = $part; } $parts[] = "\\"; @@ -253,13 +252,13 @@ public static function getIdentifierParts(string $identifier): array if (($pos = strpos($identifier, "::")) !== false) { if ($pos > 0) { $part = substr($identifier, 0, $pos); - assert(is_string($part) && $part !== ""); + assert($part !== ""); $parts[] = $part; } $parts[] = "::"; $identifier = substr($identifier, $pos + 2); } - if ($identifier !== "" && $identifier !== false) { + if ($identifier !== "") { $parts[] = $identifier; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index b90ffbc387d..fa05aef2178 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -179,9 +179,6 @@ public static function analyze( } if ($literal_concat) { - // Bypass opcache bug: https://github.com/php/php-src/issues/10635 - (function (int $_): void { - })($combinations); if (count($result_type_parts) === 0) { throw new AssertionError("The number of parts cannot be 0!"); } diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index f62cfdc7e28..06f6178844b 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -426,7 +426,6 @@ public static function getPsalmTypeFromReflectionType(?ReflectionType $reflectio if ($reflection_type instanceof ReflectionNamedType) { $type = $reflection_type->getName(); } elseif ($reflection_type instanceof ReflectionUnionType) { - /** @psalm-suppress MixedArgument */ $type = implode( '|', array_map( diff --git a/src/Psalm/Internal/LanguageServer/Client/Workspace.php b/src/Psalm/Internal/LanguageServer/Client/Workspace.php index f9d9cf39e90..cbf526ea12a 100644 --- a/src/Psalm/Internal/LanguageServer/Client/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Client/Workspace.php @@ -4,7 +4,6 @@ namespace Psalm\Internal\LanguageServer\Client; -use Amp\Promise; use JsonMapper; use Psalm\Internal\LanguageServer\ClientHandler; use Psalm\Internal\LanguageServer\LanguageServer; @@ -42,11 +41,11 @@ public function __construct(ClientHandler $handler, JsonMapper $mapper, Language * @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 + public function requestConfiguration(string $section, ?string $scopeUri = null): object { $this->server->logDebug("workspace/configuration"); - /** @var Promise */ + /** @var object */ return $this->handler->request('workspace/configuration', [ 'items' => [ [ diff --git a/src/Psalm/Internal/LanguageServer/ClientHandler.php b/src/Psalm/Internal/LanguageServer/ClientHandler.php index 06e48e3d143..d935bef2358 100644 --- a/src/Psalm/Internal/LanguageServer/ClientHandler.php +++ b/src/Psalm/Internal/LanguageServer/ClientHandler.php @@ -8,11 +8,7 @@ use AdvancedJsonRpc\Request; use AdvancedJsonRpc\Response; use AdvancedJsonRpc\SuccessResponse; -use Amp\Deferred; -use Amp\Promise; -use Generator; - -use function Amp\call; +use Amp\DeferredFuture; /** * @internal @@ -37,24 +33,19 @@ public function __construct(ProtocolReader $protocolReader, ProtocolWriter $prot * * @param string $method The method to call * @param array|object $params The method parameters - * @return Promise Resolved with the result of the request or rejected with an error + * @return mixed Resolved with the result of the request or rejected with an error */ - public function request(string $method, $params): Promise + public function request(string $method, $params) { $id = $this->idGenerator->generate(); - return call( - /** - * @return Generator> - */ - function () use ($id, $method, $params): Generator { - yield $this->protocolWriter->write( + $this->protocolWriter->write( new Message( new Request($id, $method, (object) $params), ), ); - $deferred = new Deferred(); + $deferred = new DeferredFuture(); $listener = function (Message $msg) use ($id, $deferred, &$listener): void { @@ -69,17 +60,15 @@ function (Message $msg) use ($id, $deferred, &$listener): void { // Received a response $this->protocolReader->removeListener('message', $listener); if (SuccessResponse::isSuccessResponse($msg->body)) { - $deferred->resolve($msg->body->result); + $deferred->complete($msg->body->result); } else { - $deferred->fail($msg->body->error); + $deferred->error($msg->body->error); } } }; $this->protocolReader->on('message', $listener); - return $deferred->promise(); - }, - ); + return $deferred->getFuture()->await(); } /** diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 9b092510e8d..cd364ea36d3 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -12,6 +12,8 @@ use Psalm\Internal\LanguageServer\Client\Progress\ProgressInterface; use Psalm\Internal\LanguageServer\Client\TextDocument as ClientTextDocument; use Psalm\Internal\LanguageServer\Client\Workspace as ClientWorkspace; +use Revolt\EventLoop; +use Throwable; use function is_null; use function json_decode; @@ -68,13 +70,13 @@ public function refreshConfiguration(): void { $capabilities = $this->server->clientCapabilities; if ($capabilities->workspace->configuration ?? false) { - $this->workspace->requestConfiguration('psalm')->onResolve(function ($error, $value): void { - if ($error) { - $this->server->logError('There was an error getting configuration'); - } else { - /** @var array $value */ - [$config] = $value; + EventLoop::queue(function (): void { + try { + /** @var object $config */ + [$config] = $this->workspace->requestConfiguration('psalm'); $this->configurationRefreshed((array) $config); + } catch (Throwable) { + $this->server->logError('There was an error getting configuration'); } }); } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index cc358753644..20ef9724c97 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -11,10 +11,6 @@ use AdvancedJsonRpc\Request; use AdvancedJsonRpc\Response; use AdvancedJsonRpc\SuccessResponse; -use Amp\Loop; -use Amp\Promise; -use Amp\Success; -use Generator; use InvalidArgumentException; use JsonMapper; use LanguageServerProtocol\ClientCapabilities; @@ -56,10 +52,9 @@ use Psalm\Internal\Provider\ProjectCacheProvider; use Psalm\Internal\Provider\Providers; use Psalm\IssueBuffer; +use Revolt\EventLoop; use Throwable; -use function Amp\asyncCoroutine; -use function Amp\call; use function array_combine; use function array_filter; use function array_keys; @@ -176,64 +171,51 @@ function (): void { ); $this->protocolReader->on( 'message', - asyncCoroutine( - /** - * @return Generator - */ - function (Message $msg): Generator { - if (!$msg->body) { - return; - } + function (Message $msg): void { + if (!$msg->body) { + return; + } - // Ignore responses, this is the handler for requests and notifications - if (Response::isResponse($msg->body)) { - return; - } + // Ignore responses, this is the handler for requests and notifications + if (Response::isResponse($msg->body)) { + return; + } - $result = null; - $error = null; - try { - // Invoke the method handler to get a result - /** - * @var Promise|null - */ - $dispatched = $this->dispatch($msg->body); - if ($dispatched !== null) { - $result = yield $dispatched; - } else { - $result = null; - } - } catch (Error $e) { - // If a ResponseError is thrown, send it back in the Response - $error = $e; - } catch (Throwable $e) { - // If an unexpected error occurred, send back an INTERNAL_ERROR error response - $error = new Error( - (string) $e, - ErrorCode::INTERNAL_ERROR, - null, - $e, - ); - } + $result = null; + $error = null; + try { + // Invoke the method handler to get a result + $result = $this->dispatch($msg->body); + } catch (Error $e) { + // If a ResponseError is thrown, send it back in the Response + $error = $e; + } catch (Throwable $e) { + // If an unexpected error occurred, send back an INTERNAL_ERROR error response + $error = new Error( + (string) $e, + ErrorCode::INTERNAL_ERROR, + null, + $e, + ); + } + if ($error !== null) { + $this->logError($error->message); + } + // Only send a Response for a Request + // Notifications do not send Responses + /** + * @psalm-suppress UndefinedPropertyFetch + * @psalm-suppress MixedArgument + */ + if (Request::isRequest($msg->body)) { if ($error !== null) { - $this->logError($error->message); - } - // Only send a Response for a Request - // Notifications do not send Responses - /** - * @psalm-suppress UndefinedPropertyFetch - * @psalm-suppress MixedArgument - */ - if (Request::isRequest($msg->body)) { - if ($error !== null) { - $responseBody = new ErrorResponse($msg->body->id, $error); - } else { - $responseBody = new SuccessResponse($msg->body->id, $result); - } - yield $this->protocolWriter->write(new Message($responseBody)); + $responseBody = new ErrorResponse($msg->body->id, $error); + } else { + $responseBody = new SuccessResponse($msg->body->id, $result); } - }, - ), + $this->protocolWriter->write(new Message($responseBody)); + } + }, ); $this->protocolReader->on( @@ -332,7 +314,7 @@ public static function run( $progress, $path_mapper, ); - Loop::run(); + EventLoop::run(); } elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { // Run a TCP Server $tcpServer = stream_socket_server('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr); @@ -356,7 +338,7 @@ public static function run( $progress, $path_mapper, ); - Loop::run(); + EventLoop::run(); } } else { // Use STDIO @@ -370,7 +352,7 @@ public static function run( $progress, $path_mapper, ); - Loop::run(); + EventLoop::run(); } } @@ -383,7 +365,7 @@ public static function run( * @param ClientInfo|null $clientInfo Information about the client * @param string|null $trace The initial trace setting. If omitted trace is disabled ('off'). * @param string|null $workDoneToken The token to be used to report progress during init. - * @psalm-return Promise + * @psalm-return InitializeResult */ public function initialize( ClientCapabilities $capabilities, @@ -391,192 +373,180 @@ public function initialize( ?string $rootUri = null, ?string $trace = null, ?string $workDoneToken = null - ): Promise { + ): InitializeResult { $this->clientInfo = $clientInfo; $this->clientCapabilities = $capabilities; $this->trace = $trace; - if ($rootUri !== null) { $this->path_mapper->configureClientRoot($this->getPathPart($rootUri)); } - return call( - /** @return Generator */ - function () use ($workDoneToken) { - $progress = $this->client->makeProgress($workDoneToken ?? uniqid('tkn', true)); - - $this->logInfo("Initializing..."); - $progress->begin('Psalm', 'initializing'); + $progress = $this->client->makeProgress($workDoneToken ?? uniqid('tkn', true)); - // Eventually, this might block on something. Leave it as a generator. - /** @psalm-suppress TypeDoesNotContainType */ - if (false) { - yield true; - } + $this->logInfo("Initializing..."); + $progress->begin('Psalm', 'initializing'); - $this->project_analyzer->serverMode($this); + $this->project_analyzer->serverMode($this); - $this->logInfo("Initializing: Getting code base..."); - $progress->update('getting code base'); + $this->logInfo("Initializing: Getting code base..."); + $progress->update('getting code base'); - $this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)..."); - $progress->update('scanning files'); - $this->codebase->scanFiles($this->project_analyzer->threads); + $this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)..."); + $progress->update('scanning files'); + $this->codebase->scanFiles($this->project_analyzer->threads); - $this->logInfo("Initializing: Registering stub files..."); - $progress->update('registering stub files'); - $this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress); + $this->logInfo("Initializing: Registering stub files..."); + $progress->update('registering stub files'); + $this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress); - if ($this->textDocument === null) { - $this->textDocument = new ServerTextDocument( - $this, - $this->codebase, - $this->project_analyzer, - ); - } + if ($this->textDocument === null) { + $this->textDocument = new ServerTextDocument( + $this, + $this->codebase, + $this->project_analyzer, + ); + } - if ($this->workspace === null) { - $this->workspace = new ServerWorkspace( - $this, - $this->codebase, - $this->project_analyzer, - ); - } + if ($this->workspace === null) { + $this->workspace = new ServerWorkspace( + $this, + $this->codebase, + $this->project_analyzer, + ); + } - $serverCapabilities = new ServerCapabilities(); + $serverCapabilities = new ServerCapabilities(); - //The server provides execute command support. - $serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']); + //The server provides execute command support. + $serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']); - $textDocumentSyncOptions = new TextDocumentSyncOptions(); + $textDocumentSyncOptions = new TextDocumentSyncOptions(); - //Open and close notifications are sent to the server. - $textDocumentSyncOptions->openClose = true; + //Open and close notifications are sent to the server. + $textDocumentSyncOptions->openClose = true; - $saveOptions = new SaveOptions(); - //The client is supposed to include the content on save. - $saveOptions->includeText = true; - $textDocumentSyncOptions->save = $saveOptions; + $saveOptions = new SaveOptions(); + //The client is supposed to include the content on save. + $saveOptions->includeText = true; + $textDocumentSyncOptions->save = $saveOptions; - /** - * Change notifications are sent to the server. See - * TextDocumentSyncKind.None, TextDocumentSyncKind.Full and - * TextDocumentSyncKind.Incremental. If omitted it defaults to - * TextDocumentSyncKind.None. - */ - if ($this->project_analyzer->onchange_line_limit === 0) { - /** - * Documents should not be synced at all. - */ - $textDocumentSyncOptions->change = TextDocumentSyncKind::NONE; - } else { - /** - * Documents are synced by always sending the full content - * of the document. - */ - $textDocumentSyncOptions->change = TextDocumentSyncKind::FULL; - } - - /** - * Defines how text documents are synced. Is either a detailed structure - * defining each notification or for backwards compatibility the - * TextDocumentSyncKind number. If omitted it defaults to - * `TextDocumentSyncKind.None`. - */ - $serverCapabilities->textDocumentSync = $textDocumentSyncOptions; - - /** - * The server provides document symbol support. - * Support "Find all symbols" - */ - $serverCapabilities->documentSymbolProvider = false; - /** - * The server provides workspace symbol support. - * Support "Find all symbols in workspace" - */ - $serverCapabilities->workspaceSymbolProvider = false; - /** - * The server provides goto definition support. - * Support "Go to definition" - */ - $serverCapabilities->definitionProvider = true; - /** - * The server provides find references support. - * Support "Find all references" - */ - $serverCapabilities->referencesProvider = false; - /** - * The server provides hover support. - * Support "Hover" - */ - $serverCapabilities->hoverProvider = true; + /** + * Change notifications are sent to the server. See + * TextDocumentSyncKind.None, TextDocumentSyncKind.Full and + * TextDocumentSyncKind.Incremental. If omitted it defaults to + * TextDocumentSyncKind.None. + */ + if ($this->project_analyzer->onchange_line_limit === 0) { + /** + * Documents should not be synced at all. + */ + $textDocumentSyncOptions->change = TextDocumentSyncKind::NONE; + } else { + /** + * Documents are synced by always sending the full content + * of the document. + */ + $textDocumentSyncOptions->change = TextDocumentSyncKind::FULL; + } - /** - * The server provides completion support. - * Support "Completion" - */ - if ($this->project_analyzer->provide_completion) { - $serverCapabilities->completionProvider = new CompletionOptions(); - /** - * The server provides support to resolve additional - * information for a completion item. - */ - $serverCapabilities->completionProvider->resolveProvider = false; - /** - * Most tools trigger completion request automatically without explicitly - * requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they - * do so when the user starts to type an identifier. For example if the user - * types `c` in a JavaScript file code complete will automatically pop up - * present `console` besides others as a completion item. Characters that - * make up identifiers don't need to be listed here. - * - * If code complete should automatically be trigger on characters not being - * valid inside an identifier (for example `.` in JavaScript) list them in - * `triggerCharacters`. - */ - $serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "]; - } + /** + * Defines how text documents are synced. Is either a detailed structure + * defining each notification or for backwards compatibility the + * TextDocumentSyncKind number. If omitted it defaults to + * `TextDocumentSyncKind.None`. + */ + $serverCapabilities->textDocumentSync = $textDocumentSyncOptions; + + /** + * The server provides document symbol support. + * Support "Find all symbols" + */ + $serverCapabilities->documentSymbolProvider = false; + /** + * The server provides workspace symbol support. + * Support "Find all symbols in workspace" + */ + $serverCapabilities->workspaceSymbolProvider = false; + /** + * The server provides goto definition support. + * Support "Go to definition" + */ + $serverCapabilities->definitionProvider = true; + /** + * The server provides find references support. + * Support "Find all references" + */ + $serverCapabilities->referencesProvider = false; + /** + * The server provides hover support. + * Support "Hover" + */ + $serverCapabilities->hoverProvider = true; + + /** + * The server provides completion support. + * Support "Completion" + */ + if ($this->project_analyzer->provide_completion) { + $serverCapabilities->completionProvider = new CompletionOptions(); + /** + * The server provides support to resolve additional + * information for a completion item. + */ + $serverCapabilities->completionProvider->resolveProvider = false; + /** + * Most tools trigger completion request automatically without explicitly + * requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they + * do so when the user starts to type an identifier. For example if the user + * types `c` in a JavaScript file code complete will automatically pop up + * present `console` besides others as a completion item. Characters that + * make up identifiers don't need to be listed here. + * + * If code complete should automatically be trigger on characters not being + * valid inside an identifier (for example `.` in JavaScript) list them in + * `triggerCharacters`. + */ + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "]; + } - /** - * Whether code action supports the `data` property which is - * preserved between a `textDocument/codeAction` and a - * `codeAction/resolve` request. - * - * Support "Code Actions" if we support data - * - * @since LSP 3.16.0 - */ - if ($this->clientCapabilities->textDocument->publishDiagnostics->dataSupport ?? false) { - $serverCapabilities->codeActionProvider = true; - } + /** + * Whether code action supports the `data` property which is + * preserved between a `textDocument/codeAction` and a + * `codeAction/resolve` request. + * + * Support "Code Actions" if we support data + * + * @since LSP 3.16.0 + */ + if ($this->clientCapabilities->textDocument->publishDiagnostics->dataSupport ?? false) { + $serverCapabilities->codeActionProvider = true; + } - /** - * The server provides signature help support. - */ - $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']); + /** + * The server provides signature help support. + */ + $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']); - if ($this->client->clientConfiguration->baseline !== null) { - $this->logInfo('Utilizing Baseline: '.$this->client->clientConfiguration->baseline); - $this->issue_baseline= ErrorBaseline::read( - new FileProvider, - $this->client->clientConfiguration->baseline, - ); - } + if ($this->client->clientConfiguration->baseline !== null) { + $this->logInfo('Utilizing Baseline: '.$this->client->clientConfiguration->baseline); + $this->issue_baseline= ErrorBaseline::read( + new FileProvider, + $this->client->clientConfiguration->baseline, + ); + } - $this->logInfo("Initializing: Complete."); - $progress->end('initialized'); + $this->logInfo("Initializing: Complete."); + $progress->end('initialized'); - /** - * Information about the server. - * - * @since LSP 3.15.0 - */ - $initializeResultServerInfo = new InitializeResultServerInfo('Psalm Language Server', PSALM_VERSION); + /** + * Information about the server. + * + * @since LSP 3.15.0 + */ + $initializeResultServerInfo = new InitializeResultServerInfo('Psalm Language Server', PSALM_VERSION); - return new InitializeResult($serverCapabilities, $initializeResultServerInfo); - }, - ); + return new InitializeResult($serverCapabilities, $initializeResultServerInfo); } /** @@ -656,12 +626,12 @@ function (array $opened, string $file_path) { */ public function doVersionedAnalysisDebounce(array $files, ?int $version = null): void { - Loop::cancel($this->versionedAnalysisDelayToken); + EventLoop::cancel($this->versionedAnalysisDelayToken); if ($this->client->clientConfiguration->onChangeDebounceMs === null) { $this->doVersionedAnalysis($files, $version); } else { /** @psalm-suppress MixedAssignment,UnusedPsalmSuppress */ - $this->versionedAnalysisDelayToken = Loop::delay( + $this->versionedAnalysisDelayToken = EventLoop::delay( $this->client->clientConfiguration->onChangeDebounceMs, fn() => $this->doVersionedAnalysis($files, $version), ); @@ -675,7 +645,7 @@ public function doVersionedAnalysisDebounce(array $files, ?int $version = null): */ public function doVersionedAnalysis(array $files, ?int $version = null): void { - Loop::cancel($this->versionedAnalysisDelayToken); + EventLoop::cancel($this->versionedAnalysisDelayToken); try { $this->logDebug("Doing Analysis from version: $version"); $this->codebase->reloadFiles( @@ -828,7 +798,7 @@ function (IssueData $issue_data) { * which they have sent a shutdown request. Clients should also wait with sending the exit notification until they * have received a response from the shutdown request. */ - public function shutdown(): Promise + public function shutdown(): void { $this->clientStatus('closing'); $this->logInfo("Shutting down..."); @@ -839,7 +809,6 @@ public function shutdown(): Promise $scanned_files, ); $this->clientStatus('closed'); - return new Success(null); } /** diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php index 35031d8541d..81bd15833b7 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php @@ -5,12 +5,10 @@ namespace Psalm\Internal\LanguageServer; use AdvancedJsonRpc\Message as MessageBody; -use Amp\ByteStream\ResourceInputStream; -use Amp\Promise; +use Amp\ByteStream\ReadableResourceStream; use Exception; -use Generator; +use Revolt\EventLoop; -use function Amp\asyncCall; use function explode; use function strlen; use function substr; @@ -45,16 +43,11 @@ class ProtocolStreamReader implements ProtocolReader */ public function __construct($input) { - $input = new ResourceInputStream($input); - asyncCall( - /** - * @return Generator, ?string, void> - */ - function () use ($input): Generator { + $input = new ReadableResourceStream($input); + EventLoop::queue( + function () use ($input): void { while ($this->is_accepting_new_requests) { - $read_promise = $input->read(); - - $chunk = yield $read_promise; + $chunk = $input->read(); if ($chunk === null) { break; diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php index 10d86e14cd9..bcf2f02b129 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php @@ -4,29 +4,28 @@ namespace Psalm\Internal\LanguageServer; -use Amp\ByteStream\ResourceOutputStream; -use Amp\Promise; +use Amp\ByteStream\WritableResourceStream; /** * @internal */ class ProtocolStreamWriter implements ProtocolWriter { - private ResourceOutputStream $output; + private WritableResourceStream $output; /** * @param resource $output */ public function __construct($output) { - $this->output = new ResourceOutputStream($output); + $this->output = new WritableResourceStream($output); } /** * {@inheritdoc} */ - public function write(Message $msg): Promise + public function write(Message $msg): void { - return $this->output->write((string)$msg); + $this->output->write((string)$msg); } } diff --git a/src/Psalm/Internal/LanguageServer/ProtocolWriter.php b/src/Psalm/Internal/LanguageServer/ProtocolWriter.php index a512012a369..9f94d6f7aac 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolWriter.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolWriter.php @@ -4,14 +4,10 @@ namespace Psalm\Internal\LanguageServer; -use Amp\Promise; - interface ProtocolWriter { /** - * Sends a Message to the client - * - * @return Promise Resolved when the message has been fully written out to the output stream + * Sends a Message to the client. */ - public function write(Message $msg): Promise; + public function write(Message $msg): void; } diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 508af5aeb9f..bf08c941998 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -4,8 +4,6 @@ namespace Psalm\Internal\LanguageServer\Server; -use Amp\Promise; -use Amp\Success; use LanguageServerProtocol\CodeAction; use LanguageServerProtocol\CodeActionContext; use LanguageServerProtocol\CodeActionKind; @@ -166,12 +164,11 @@ public function didClose(TextDocumentIdentifier $textDocument): void * * @param TextDocumentIdentifier $textDocument The text document * @param Position $position The position inside the text document - * @psalm-return Promise|Promise */ - public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise + public function definition(TextDocumentIdentifier $textDocument, Position $position): ?Location { if (!$this->server->client->clientConfiguration->provideDefinition) { - return new Success(null); + return null; } $this->server->logDebug( @@ -182,34 +179,32 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { - return new Success(null); + return null; } try { $reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position); } catch (UnanalyzedFileException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } if ($reference === null) { - return new Success(null); + return null; } $code_location = $this->codebase->getSymbolLocationByReference($reference); if (!$code_location) { - return new Success(null); + return null; } - return new Success( - new Location( - $this->server->pathToUri($code_location->file_path), - new Range( - new Position($code_location->getLineNumber() - 1, $code_location->getColumn() - 1), - new Position($code_location->getEndLineNumber() - 1, $code_location->getEndColumn() - 1), - ), + return new Location( + $this->server->pathToUri($code_location->file_path), + new Range( + new Position($code_location->getLineNumber() - 1, $code_location->getColumn() - 1), + new Position($code_location->getEndLineNumber() - 1, $code_location->getEndColumn() - 1), ), ); } @@ -220,12 +215,11 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit * * @param TextDocumentIdentifier $textDocument The text document * @param Position $position The position inside the text document - * @psalm-return Promise|Promise */ - public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise + public function hover(TextDocumentIdentifier $textDocument, Position $position): ?Hover { if (!$this->server->client->clientConfiguration->provideHover) { - return new Success(null); + return null; } $this->server->logDebug( @@ -236,32 +230,32 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { - return new Success(null); + return null; } try { $reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position); } catch (UnanalyzedFileException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } if ($reference === null) { - return new Success(null); + return null; } try { $markup = $this->codebase->getMarkupContentForSymbolByReference($reference); } catch (UnexpectedValueException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } if ($markup === null) { - return new Success(null); + return null; } - return new Success(new Hover($markup, $reference->range)); + return new Hover($markup, $reference->range); } /** @@ -276,12 +270,11 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): * * @param TextDocumentIdentifier $textDocument The text document * @param Position $position The position - * @psalm-return Promise>|Promise|Promise */ - public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise + public function completion(TextDocumentIdentifier $textDocument, Position $position): ?CompletionList { if (!$this->server->client->clientConfiguration->provideCompletion) { - return new Success(null); + return null; } $this->server->logDebug( @@ -292,7 +285,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { - return new Success(null); + return null; } try { @@ -314,42 +307,42 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit $file_path, ); } - return new Success(new CompletionList($completion_items, false)); + return new CompletionList($completion_items, false); } } catch (UnanalyzedFileException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } catch (TypeParseTreeException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } try { $type_context = $this->codebase->getTypeContextAtPosition($file_path, $position); if ($type_context) { $completion_items = $this->codebase->getCompletionItemsForType($type_context); - return new Success(new CompletionList($completion_items, false)); + return new CompletionList($completion_items, false); } } catch (UnexpectedValueException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } catch (TypeParseTreeException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } $this->server->logError('completion not found at ' . $position->line . ':' . $position->character); - return new Success(null); + return null; } /** * The signature help request is sent from the client to the server to request signature * information at a given cursor position. */ - public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise + public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): ?SignatureHelp { if (!$this->server->client->clientConfiguration->provideSignatureHelp) { - return new Success(null); + return null; } $this->server->logDebug( @@ -360,37 +353,35 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { - return new Success(null); + return null; } try { $argument_location = $this->codebase->getFunctionArgumentAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } if ($argument_location === null) { - return new Success(null); + return null; } try { $signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path); } catch (UnexpectedValueException $e) { $this->server->logThrowable($e); - return new Success(null); + return null; } if (!$signature_information) { - return new Success(null); + return null; } - return new Success( - new SignatureHelp( - [$signature_information], - 0, - $argument_location[1], - ), + return new SignatureHelp( + [$signature_information], + 0, + $argument_location[1], ); } @@ -399,10 +390,10 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po * for a given text document and range. These commands are typically code fixes to * either fix problems or to beautify/refactor code. */ - public function codeAction(TextDocumentIdentifier $textDocument, CodeActionContext $context): Promise + public function codeAction(TextDocumentIdentifier $textDocument, CodeActionContext $context): ?array { if (!$this->server->client->clientConfiguration->provideCodeActions) { - return new Success(null); + return null; } $this->server->logDebug( @@ -413,7 +404,7 @@ public function codeAction(TextDocumentIdentifier $textDocument, CodeActionConte //Don't report code actions for files we arent watching if (!$this->codebase->config->isInProjectDirs($file_path)) { - return new Success(null); + return null; } $fixers = []; @@ -477,11 +468,9 @@ public function codeAction(TextDocumentIdentifier $textDocument, CodeActionConte } if (empty($fixers)) { - return new Success(null); + return null; } - return new Success( - array_values($fixers), - ); + return array_values($fixers); } } diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 6d2e1622575..129fa4e5b9c 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -4,8 +4,6 @@ namespace Psalm\Internal\LanguageServer\Server; -use Amp\Promise; -use Amp\Success; use InvalidArgumentException; use LanguageServerProtocol\FileChangeType; use LanguageServerProtocol\FileEvent; @@ -125,7 +123,7 @@ public function didChangeConfiguration(): void * @param mixed $arguments * @psalm-suppress PossiblyUnusedMethod */ - public function executeCommand(string $command, $arguments): Promise + public function executeCommand(string $command, $arguments): void { $this->server->logDebug( 'workspace/executeCommand', @@ -154,7 +152,5 @@ public function executeCommand(string $command, $arguments): Promise $this->server->emitVersionedIssues([$file => $arguments['uri']]); break; } - - return new Success(null); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php index 88125e398e3..62e7cbcb884 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php @@ -13,7 +13,6 @@ use Psalm\Type\Union; use function array_combine; -use function assert; use function count; /** @@ -121,8 +120,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $values, ); - assert($result !== false); - if (!$result) { return Type::getEmptyArray(); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php index 8c9af9410bc..925bc0c1a92 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php @@ -201,35 +201,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } } - if ($result === false && count($dummy) === $provided_placeholders_count) { - // could be invalid format or too few arguments - // we cannot distinguish this in PHP 7 without additional checks - $max_dummy = array_fill(0, 100, ''); - $result = @sprintf($type->getSingleStringLiteral()->value, ...$max_dummy); - if ($result === false) { - // the format is invalid - IssueBuffer::maybeAdd( - new InvalidArgument( - 'Argument 1 of ' . $event->getFunctionId() . ' is invalid', - $event->getCodeLocation(), - $event->getFunctionId(), - ), - $statements_source->getSuppressedIssues(), - ); - } else { - IssueBuffer::maybeAdd( - new TooFewArguments( - 'Too few arguments for ' . $event->getFunctionId(), - $event->getCodeLocation(), - $event->getFunctionId(), - ), - $statements_source->getSuppressedIssues(), - ); - } - - return Type::getFalse(); - } - // we can only validate the format and arg 1 when using splat if ($has_splat_args === true) { break; @@ -264,13 +235,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return null; } - if ($initial_result !== null && $initial_result !== false && $initial_result !== '') { + if ($initial_result !== null && $initial_result !== '') { return Type::getNonEmptyString(); } // if we didn't have any valid result // the pattern is invalid or not yet supported by the return type provider - if ($initial_result === null || $initial_result === false) { + if ($initial_result === null) { return null; } diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 074ef233ab3..8d36c9990db 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1109,7 +1109,7 @@ private static function adjustTKeyedArrayType( $base_key = implode($key_parts); - if (isset($existing_types[$base_key]) && $array_key_offset !== false) { + if (isset($existing_types[$base_key]) && $array_key_offset !== '') { foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { if ($base_atomic_type instanceof TList) { $base_atomic_type = $base_atomic_type->getKeyedArray(); diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index e3e99991c13..c92f7924238 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -606,7 +606,6 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa public function assertEntryReturnType(ReflectionFunctionAbstract $function, string $entryReturnType): void { if (version_compare(PHP_VERSION, '8.1.0', '>=')) { - /** @var ReflectionType|null $expectedType */ $expectedType = $function->hasTentativeReturnType() ? $function->getTentativeReturnType() : $function->getReturnType(); } else { $expectedType = $function->getReturnType(); diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index d8814ec3633..d6eee08e893 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -2,7 +2,7 @@ namespace Psalm\Tests\LanguageServer; -use Amp\Deferred; +use Amp\DeferredFuture; use Psalm\Codebase; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; @@ -22,7 +22,6 @@ use Psalm\Tests\LanguageServer\MockProtocolStream; use Psalm\Tests\TestConfig; -use function Amp\Promise\wait; use function getcwd; use function rand; @@ -67,7 +66,7 @@ public function setUp(): void public function testSnippetSupportDisabled(): void { // Create a new promisor - $deferred = new Deferred; + $deferred = new DeferredFuture; $this->setTimeout(5000); $clientConfiguration = new ClientConfiguration(); @@ -94,7 +93,7 @@ public function testSnippetSupportDisabled(): void /** @psalm-suppress NullPropertyFetch,PossiblyNullPropertyFetch,UndefinedPropertyFetch */ if ($message->body->method === 'telemetry/event' && ($message->body->params->message ?? null) === 'initialized') { $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); - $deferred->resolve(null); + $deferred->complete(null); return; } @@ -104,12 +103,12 @@ public function testSnippetSupportDisabled(): void && ($message->body->params->value->message ?? null) === 'initialized' ) { $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); - $deferred->resolve(null); + $deferred->complete(null); return; } }); - wait($deferred->promise()); + $deferred->getFuture()->await(); } /** diff --git a/tests/LanguageServer/MockProtocolStream.php b/tests/LanguageServer/MockProtocolStream.php index 34f8072c9cc..f3db6aebd95 100644 --- a/tests/LanguageServer/MockProtocolStream.php +++ b/tests/LanguageServer/MockProtocolStream.php @@ -4,14 +4,12 @@ namespace Psalm\Tests\LanguageServer; -use Amp\Deferred; -use Amp\Loop; -use Amp\Promise; use Psalm\Internal\LanguageServer\EmitterInterface; use Psalm\Internal\LanguageServer\EmitterTrait; use Psalm\Internal\LanguageServer\Message; use Psalm\Internal\LanguageServer\ProtocolReader; use Psalm\Internal\LanguageServer\ProtocolWriter; +use Revolt\EventLoop; /** * A fake duplex protocol stream @@ -21,20 +19,11 @@ class MockProtocolStream implements ProtocolReader, ProtocolWriter, EmitterInter use EmitterTrait; /** * Sends a Message to the client - * - * @psalm-suppress PossiblyUnusedReturnValue */ - public function write(Message $msg): Promise + public function write(Message $msg): void { - Loop::defer(function () use ($msg): void { + EventLoop::queue(function () use ($msg): void { $this->emit('message', [Message::parse((string)$msg)]); }); - - // Create a new promisor - $deferred = new Deferred; - - $deferred->resolve(null); - - return $deferred->promise(); } } diff --git a/tests/fixtures/DummyProjectWithErrors/composer.json b/tests/fixtures/DummyProjectWithErrors/composer.json index af6d9be31ad..905448a4daa 100644 --- a/tests/fixtures/DummyProjectWithErrors/composer.json +++ b/tests/fixtures/DummyProjectWithErrors/composer.json @@ -3,8 +3,7 @@ "description": "A sample project to be used when testing Psalm", "type": "project", "require": { - "php": ">= 7.1", - "vimeo/psalm": "^4.3" + "php": ">= 7.1" }, "autoload": { "psr-4": {