diff --git a/.gitignore b/.gitignore
index 0d602a01f44..064fa341407 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,7 @@
/vendor-bin/*/composer.lock
/vendor-bin/*/vendor/
/tests/fixtures/symlinktest/*
-
+.vscode
.idea/
.vscode/
.php-version
diff --git a/composer.json b/composer.json
index 48731769287..31ae6807fea 100644
--- a/composer.json
+++ b/composer.json
@@ -44,6 +44,7 @@
},
"require-dev": {
"ext-curl": "*",
+ "amphp/phpunit-util": "^2.0",
"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 2aa83e33ad9..e53cd8c1c17 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -16,8 +16,6 @@
$const_name
$const_name
- $matches[0]
- $property_name
$symbol_name
$symbol_parts[1]
@@ -227,11 +225,6 @@
expr->getArgs()[0]]]>
-
-
-
-
-
$identifier_name
@@ -292,35 +285,6 @@
props[0]]]>
-
-
-
- 4]]>
- 4]]>
-
-
-
-
- $capabilities
- $processId
- $rootPath
-
-
-
-
- $pair[1]
-
-
-
-
- $parts[1]
-
-
-
-
- $contentChanges[0]
-
-
$method_id_parts[1]
diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php
index 04790dfcc73..0bdfd64a683 100644
--- a/src/Psalm/CodeLocation.php
+++ b/src/Psalm/CodeLocation.php
@@ -169,6 +169,7 @@ private function calculateRealLocation(): void
$codebase = $project_analyzer->getCodebase();
+ /** @psalm-suppress ImpureMethodCall */
$file_contents = $codebase->getFileContents($this->file_path);
$file_length = strlen($file_contents);
diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php
index 2e51cd4975a..d6caa46d1da 100644
--- a/src/Psalm/Codebase.php
+++ b/src/Psalm/Codebase.php
@@ -37,6 +37,8 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\DataFlow\TaintSink;
use Psalm\Internal\DataFlow\TaintSource;
+use Psalm\Internal\LanguageServer\PHPMarkdownContent;
+use Psalm\Internal\LanguageServer\Reference;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Provider\ClassLikeStorageProvider;
use Psalm\Internal\Provider\FileProvider;
@@ -67,8 +69,10 @@
use UnexpectedValueException;
use function array_combine;
+use function array_merge;
use function array_pop;
use function array_reverse;
+use function array_values;
use function count;
use function dirname;
use function error_log;
@@ -82,6 +86,7 @@
use function ksort;
use function preg_match;
use function preg_replace;
+use function str_replace;
use function strlen;
use function strpos;
use function strrpos;
@@ -390,15 +395,19 @@ private function loadAnalyzer(): void
/**
* @param array $candidate_files
*/
- public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files): void
+ public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files, bool $force = false): void
{
$this->loadAnalyzer();
+ if ($force) {
+ FileReferenceProvider::clearCache();
+ }
+
$this->file_reference_provider->loadReferenceCache(false);
FunctionLikeAnalyzer::clearCache();
- if (!$this->statements_provider->parser_cache_provider) {
+ if ($force || !$this->statements_provider->parser_cache_provider) {
$diff_files = $candidate_files;
} else {
$diff_files = [];
@@ -500,7 +509,6 @@ public function scanFiles(int $threads = 1): void
}
}
- /** @psalm-mutation-free */
public function getFileContents(string $file_path): string
{
return $this->file_provider->getContents($file_path);
@@ -593,6 +601,7 @@ public function findReferencesToMethod(string $method_id): array
*/
public function findReferencesToProperty(string $property_id): array
{
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
[$fq_class_name, $property_name] = explode('::', $property_id);
return $this->file_reference_provider->getClassPropertyLocations(
@@ -970,7 +979,225 @@ public function getFunctionStorageForSymbol(string $file_path, string $symbol):
}
/**
- * @return array{ type: string, description?: string|null}|null
+ * Get Markup content from Reference
+ */
+ public function getMarkupContentForSymbolByReference(
+ Reference $reference
+ ): ?PHPMarkdownContent {
+ //Direct Assignment
+ if (is_numeric($reference->symbol[0])) {
+ return new PHPMarkdownContent(
+ preg_replace(
+ '/^[^:]*:/',
+ '',
+ $reference->symbol,
+ ),
+ );
+ }
+
+ //Class
+ if (strpos($reference->symbol, '::')) {
+ //Class Method
+ if (strpos($reference->symbol, '()')) {
+ $symbol = substr($reference->symbol, 0, -2);
+
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $method_id = new MethodIdentifier(...explode('::', $symbol));
+
+ $declaring_method_id = $this->methods->getDeclaringMethodId(
+ $method_id,
+ );
+
+ if (!$declaring_method_id) {
+ return null;
+ }
+
+ $storage = $this->methods->getStorage($declaring_method_id);
+
+ return new PHPMarkdownContent(
+ $storage->getHoverMarkdown(),
+ "{$storage->defining_fqcln}::{$storage->cased_name}",
+ $storage->description,
+ );
+ }
+
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
+ [, $symbol_name] = explode('::', $reference->symbol);
+
+ //Class Property
+ if (strpos($reference->symbol, '$') !== false) {
+ $property_id = preg_replace('/^\\\\/', '', $reference->symbol);
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
+ [$fq_class_name, $property_name] = explode('::$', $property_id);
+ $class_storage = $this->classlikes->getStorageFor($fq_class_name);
+
+ //Get Real Properties
+ if (isset($class_storage->declaring_property_ids[$property_name])) {
+ $declaring_property_class = $class_storage->declaring_property_ids[$property_name];
+ $declaring_class_storage = $this->classlike_storage_provider->get($declaring_property_class);
+
+ if (isset($declaring_class_storage->properties[$property_name])) {
+ $storage = $declaring_class_storage->properties[$property_name];
+ return new PHPMarkdownContent(
+ "{$storage->getInfo()} {$symbol_name}",
+ $reference->symbol,
+ $storage->description,
+ );
+ }
+ }
+
+ //Get Docblock properties
+ if (isset($class_storage->pseudo_property_set_types['$'.$property_name])) {
+ return new PHPMarkdownContent(
+ 'public '.
+ (string) $class_storage->pseudo_property_set_types['$'.$property_name].' $'.$property_name,
+ $reference->symbol,
+ );
+ }
+
+ //Get Docblock properties
+ if (isset($class_storage->pseudo_property_get_types['$'.$property_name])) {
+ return new PHPMarkdownContent(
+ 'public '.
+ (string) $class_storage->pseudo_property_get_types['$'.$property_name].' $'.$property_name,
+ $reference->symbol,
+ );
+ }
+
+ return null;
+ }
+
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
+ [$fq_classlike_name, $const_name] = explode(
+ '::',
+ $reference->symbol,
+ );
+
+ $class_constants = $this->classlikes->getConstantsForClass(
+ $fq_classlike_name,
+ ReflectionProperty::IS_PRIVATE,
+ );
+
+ if (!isset($class_constants[$const_name])) {
+ return null;
+ }
+
+ //Class Constant
+ return new PHPMarkdownContent(
+ $class_constants[$const_name]->getHoverMarkdown($const_name),
+ $fq_classlike_name . '::' . $const_name,
+ $class_constants[$const_name]->description,
+ );
+ }
+
+ //Procedural Function
+ if (strpos($reference->symbol, '()')) {
+ $function_id = strtolower(substr($reference->symbol, 0, -2));
+ $file_storage = $this->file_storage_provider->get(
+ $reference->file_path,
+ );
+
+ if (isset($file_storage->functions[$function_id])) {
+ $function_storage = $file_storage->functions[$function_id];
+
+ return new PHPMarkdownContent(
+ $function_storage->getHoverMarkdown(),
+ $function_id,
+ $function_storage->description,
+ );
+ }
+
+ if (!$function_id) {
+ return null;
+ }
+
+ $function = $this->functions->getStorage(null, $function_id);
+
+ return new PHPMarkdownContent(
+ $function->getHoverMarkdown(),
+ $function_id,
+ $function->description,
+ );
+ }
+
+ //Procedural Variable
+ if (strpos($reference->symbol, '$') === 0) {
+ $type = VariableFetchAnalyzer::getGlobalType($reference->symbol, $this->analysis_php_version_id);
+ if (!$type->isMixed()) {
+ return new PHPMarkdownContent(
+ (string) $type,
+ $reference->symbol,
+ );
+ }
+ }
+
+ try {
+ $storage = $this->classlike_storage_provider->get(
+ $reference->symbol,
+ );
+ return new PHPMarkdownContent(
+ ($storage->abstract ? 'abstract ' : '') .
+ 'class ' .
+ $storage->name,
+ $storage->name,
+ $storage->description,
+ );
+ } catch (InvalidArgumentException $e) {
+ //continue on as normal
+ }
+
+ if (strpos($reference->symbol, '\\')) {
+ $const_name_parts = explode('\\', $reference->symbol);
+ $const_name = array_pop($const_name_parts);
+ $namespace_name = implode('\\', $const_name_parts);
+
+ $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace(
+ $namespace_name,
+ ReflectionProperty::IS_PUBLIC,
+ );
+ //Namespace Constant
+ if (isset($namespace_constants[$const_name])) {
+ $type = $namespace_constants[$const_name];
+ return new PHPMarkdownContent(
+ $reference->symbol . ' ' . $type,
+ $reference->symbol,
+ );
+ }
+ } else {
+ $file_storage = $this->file_storage_provider->get(
+ $reference->file_path,
+ );
+ // ?
+ if (isset($file_storage->constants[$reference->symbol])) {
+ return new PHPMarkdownContent(
+ 'const ' .
+ $reference->symbol .
+ ' ' .
+ $file_storage->constants[$reference->symbol],
+ $reference->symbol,
+ );
+ }
+ $type = ConstFetchAnalyzer::getGlobalConstType(
+ $this,
+ $reference->symbol,
+ $reference->symbol,
+ );
+
+ //Global Constant
+ if ($type) {
+ return new PHPMarkdownContent(
+ 'const ' . $reference->symbol . ' ' . $type,
+ $reference->symbol,
+ );
+ }
+ }
+
+ return new PHPMarkdownContent($reference->symbol);
+ }
+
+ /**
+ * @psalm-suppress PossiblyUnusedMethod
+ * @deprecated will be removed in Psalm 6. use {@see Codebase::getSymbolLocationByReference()} instead
*/
public function getSymbolInformation(string $file_path, string $symbol): ?array
{
@@ -995,7 +1222,7 @@ public function getSymbolInformation(string $file_path, string $symbol): ?array
$storage = $this->methods->getStorage($declaring_method_id);
return [
- 'type' => 'getSignature(true),
+ 'type' => 'getCompletionSignature(),
'description' => $storage->description,
];
}
@@ -1036,7 +1263,7 @@ public function getSymbolInformation(string $file_path, string $symbol): ?array
$function_storage = $file_storage->functions[$function_id];
return [
- 'type' => 'getSignature(true),
+ 'type' => 'getCompletionSignature(),
'description' => $function_storage->description,
];
}
@@ -1047,7 +1274,7 @@ public function getSymbolInformation(string $file_path, string $symbol): ?array
$function = $this->functions->getStorage(null, $function_id);
return [
- 'type' => 'getSignature(true),
+ 'type' => 'getCompletionSignature(),
'description' => $function->description,
];
}
@@ -1100,6 +1327,10 @@ public function getSymbolInformation(string $file_path, string $symbol): ?array
}
}
+ /**
+ * @psalm-suppress PossiblyUnusedMethod
+ * @deprecated will be removed in Psalm 6. use {@see Codebase::getSymbolLocationByReference()} instead
+ */
public function getSymbolLocation(string $file_path, string $symbol): ?CodeLocation
{
if (is_numeric($symbol[0])) {
@@ -1182,11 +1413,127 @@ public function getSymbolLocation(string $file_path, string $symbol): ?CodeLocat
}
}
+ public function getSymbolLocationByReference(Reference $reference): ?CodeLocation
+ {
+ if (is_numeric($reference->symbol[0])) {
+ $symbol = preg_replace('/:.*/', '', $reference->symbol);
+ $symbol_parts = explode('-', $symbol);
+
+ if (!isset($symbol_parts[0]) || !isset($symbol_parts[1])) {
+ return null;
+ }
+
+ $file_contents = $this->getFileContents($reference->file_path);
+
+ return new Raw(
+ $file_contents,
+ $reference->file_path,
+ $this->config->shortenFileName($reference->file_path),
+ (int) $symbol_parts[0],
+ (int) $symbol_parts[1],
+ );
+ }
+
+ try {
+ if (strpos($reference->symbol, '::')) {
+ if (strpos($reference->symbol, '()')) {
+ $symbol = substr($reference->symbol, 0, -2);
+
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $method_id = new MethodIdentifier(
+ ...explode('::', $symbol),
+ );
+
+ $declaring_method_id = $this->methods->getDeclaringMethodId(
+ $method_id,
+ );
+
+ if (!$declaring_method_id) {
+ return null;
+ }
+
+ $storage = $this->methods->getStorage($declaring_method_id);
+
+ return $storage->location;
+ }
+
+ if (strpos($reference->symbol, '$') !== false) {
+ $storage = $this->properties->getStorage(
+ $reference->symbol,
+ );
+
+ return $storage->location;
+ }
+
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
+ [$fq_classlike_name, $const_name] = explode(
+ '::',
+ $reference->symbol,
+ );
+
+ $class_constants = $this->classlikes->getConstantsForClass(
+ $fq_classlike_name,
+ ReflectionProperty::IS_PRIVATE,
+ );
+
+ if (!isset($class_constants[$const_name])) {
+ return null;
+ }
+
+ return $class_constants[$const_name]->location;
+ }
+
+ if (strpos($reference->symbol, '()')) {
+ $file_storage = $this->file_storage_provider->get(
+ $reference->file_path,
+ );
+
+ $function_id = strtolower(substr($reference->symbol, 0, -2));
+
+ if (isset($file_storage->functions[$function_id])) {
+ return $file_storage->functions[$function_id]->location;
+ }
+
+ if (!$function_id) {
+ return null;
+ }
+
+ return $this->functions->getStorage(null, $function_id)
+ ->location;
+ }
+
+ return $this->classlike_storage_provider->get(
+ $reference->symbol,
+ )->location;
+ } catch (UnexpectedValueException $e) {
+ error_log($e->getMessage());
+
+ return null;
+ } catch (InvalidArgumentException $e) {
+ return null;
+ }
+ }
+
/**
+ * @psalm-suppress PossiblyUnusedMethod
* @return array{0: string, 1: Range}|null
*/
public function getReferenceAtPosition(string $file_path, Position $position): ?array
{
+ $ref = $this->getReferenceAtPositionAsReference($file_path, $position);
+ if ($ref === null) {
+ return null;
+ }
+ return [$ref->symbol, $ref->range];
+ }
+
+ /**
+ * Get Reference from Position
+ */
+ public function getReferenceAtPositionAsReference(
+ string $file_path,
+ Position $position
+ ): ?Reference {
$is_open = $this->file_provider->isOpen($file_path);
if (!$is_open) {
@@ -1197,33 +1544,37 @@ public function getReferenceAtPosition(string $file_path, Position $position): ?
$offset = $position->toOffset($file_contents);
- [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path);
-
- $reference = null;
-
- if (!$reference_map && !$type_map) {
- return null;
- }
+ $reference_maps = $this->analyzer->getMapsForFile($file_path);
$reference_start_pos = null;
$reference_end_pos = null;
+ $symbol = null;
- ksort($reference_map);
+ foreach ($reference_maps as $reference_map) {
+ ksort($reference_map);
- foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) {
- if ($offset < $start_pos) {
- break;
+ foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) {
+ if ($offset < $start_pos) {
+ break;
+ }
+
+ if ($offset > $end_pos) {
+ continue;
+ }
+ $reference_start_pos = $start_pos;
+ $reference_end_pos = $end_pos;
+ $symbol = $possible_reference;
}
- if ($offset > $end_pos) {
- continue;
+ if ($symbol !== null &&
+ $reference_start_pos !== null &&
+ $reference_end_pos !== null
+ ) {
+ break;
}
- $reference_start_pos = $start_pos;
- $reference_end_pos = $end_pos;
- $reference = $possible_reference;
}
- if ($reference === null || $reference_start_pos === null || $reference_end_pos === null) {
+ if ($symbol === null || $reference_start_pos === null || $reference_end_pos === null) {
return null;
}
@@ -1232,7 +1583,7 @@ public function getReferenceAtPosition(string $file_path, Position $position): ?
self::getPositionFromOffset($reference_end_pos, $file_contents),
);
- return [$reference, $range];
+ return new Reference($file_path, $symbol, $range);
}
/**
@@ -1399,6 +1750,7 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio
continue;
}
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
$num_whitespace_bytes = preg_match('/\G\s+/', $file_contents, $matches, 0, $end_pos_excluding_whitespace)
? strlen($matches[0])
: 0;
@@ -1487,8 +1839,11 @@ public function getTypeContextAtPosition(string $file_path, Position $position):
/**
* @return list
*/
- public function getCompletionItemsForClassishThing(string $type_string, string $gap): array
- {
+ public function getCompletionItemsForClassishThing(
+ string $type_string,
+ string $gap,
+ bool $snippets_supported = false
+ ): array {
$completion_items = [];
$type = Type::parseString($type_string);
@@ -1505,11 +1860,11 @@ public function getCompletionItemsForClassishThing(string $type_string, string $
$completion_item = new CompletionItem(
$method_storage->cased_name,
CompletionItemKind::METHOD,
- (string)$method_storage,
+ $method_storage->getCompletionSignature(),
$method_storage->description,
(string)$method_storage->visibility,
$method_storage->cased_name,
- $method_storage->cased_name . (count($method_storage->params) !== 0 ? '($0)' : '()'),
+ $method_storage->cased_name,
null,
null,
new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'),
@@ -1517,12 +1872,47 @@ public function getCompletionItemsForClassishThing(string $type_string, string $
2,
);
- $completion_item->insertTextFormat = InsertTextFormat::SNIPPET;
+ if ($snippets_supported && count($method_storage->params) > 0) {
+ $completion_item->insertText .= '($0)';
+ $completion_item->insertTextFormat =
+ InsertTextFormat::SNIPPET;
+ } else {
+ $completion_item->insertText .= '()';
+ }
$completion_items[] = $completion_item;
}
}
+ $pseudo_property_types = [];
+ foreach ($class_storage->pseudo_property_get_types as $property_name => $type) {
+ $pseudo_property_types[$property_name] = new CompletionItem(
+ str_replace('$', '', $property_name),
+ CompletionItemKind::PROPERTY,
+ $type->__toString(),
+ null,
+ '1', //sort text
+ str_replace('$', '', $property_name),
+ ($gap === '::' ? '$' : '') .
+ str_replace('$', '', $property_name),
+ );
+ }
+
+ foreach ($class_storage->pseudo_property_set_types as $property_name => $type) {
+ $pseudo_property_types[$property_name] = new CompletionItem(
+ str_replace('$', '', $property_name),
+ CompletionItemKind::PROPERTY,
+ $type->__toString(),
+ null,
+ '1',
+ str_replace('$', '', $property_name),
+ ($gap === '::' ? '$' : '') .
+ str_replace('$', '', $property_name),
+ );
+ }
+
+ $completion_items = array_merge($completion_items, array_values($pseudo_property_types));
+
foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) {
$property_storage = $this->properties->getStorage(
$declaring_class . '::$' . $property_name,
@@ -1715,7 +2105,7 @@ public function getCompletionItemsForPartialSymbol(
$completion_items[] = new CompletionItem(
$function_name,
CompletionItemKind::FUNCTION,
- $function->getSignature(false),
+ $function->getCompletionSignature(),
$function->description,
null,
$function_name,
@@ -1832,9 +2222,9 @@ private static function getPositionFromOffset(int $offset, string $file_contents
);
}
- public function addTemporaryFileChanges(string $file_path, string $new_content): void
+ public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void
{
- $this->file_provider->addTemporaryFileChanges($file_path, $new_content);
+ $this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version);
}
public function removeTemporaryFileChanges(string $file_path): void
diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
index b874537f0c3..6c3d39f2ff4 100644
--- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
@@ -2,7 +2,6 @@
namespace Psalm\Internal\Analyzer;
-use Amp\Loop;
use Fidry\CpuCoreCounter\CpuCoreCounter;
use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound;
use InvalidArgumentException;
@@ -15,8 +14,6 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\LanguageServer\LanguageServer;
-use Psalm\Internal\LanguageServer\ProtocolStreamReader;
-use Psalm\Internal\LanguageServer\ProtocolStreamWriter;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Provider\ClassLikeStorageProvider;
use Psalm\Internal\Provider\FileProvider;
@@ -65,7 +62,6 @@
use function array_merge;
use function array_shift;
use function clearstatcache;
-use function cli_set_process_title;
use function count;
use function defined;
use function dirname;
@@ -82,14 +78,9 @@
use function microtime;
use function mkdir;
use function number_format;
-use function pcntl_fork;
use function preg_match;
use function rename;
use function sprintf;
-use function stream_set_blocking;
-use function stream_socket_accept;
-use function stream_socket_client;
-use function stream_socket_server;
use function strlen;
use function strpos;
use function strtolower;
@@ -102,8 +93,6 @@
use const PHP_VERSION;
use const PSALM_VERSION;
use const STDERR;
-use const STDIN;
-use const STDOUT;
/**
* @internal
@@ -211,16 +200,6 @@ class ProjectAnalyzer
UnnecessaryVarAnnotation::class,
];
- /**
- * When this is true, the language server will send the diagnostic code with a help link.
- */
- public bool $language_server_use_extended_diagnostic_codes = false;
-
- /**
- * If this is true then the language server will send log messages to the client with additional information.
- */
- public bool $language_server_verbose = false;
-
/**
* @param array $generated_report_options
*/
@@ -230,12 +209,21 @@ public function __construct(
?ReportOptions $stdout_report_options = null,
array $generated_report_options = [],
int $threads = 1,
- ?Progress $progress = null
+ ?Progress $progress = null,
+ ?Codebase $codebase = null
) {
if ($progress === null) {
$progress = new VoidProgress();
}
+ if ($codebase === null) {
+ $codebase = new Codebase(
+ $config,
+ $providers,
+ $progress,
+ );
+ }
+
$this->parser_cache_provider = $providers->parser_cache_provider;
$this->project_cache_provider = $providers->project_cache_provider;
$this->file_provider = $providers->file_provider;
@@ -248,11 +236,7 @@ public function __construct(
$this->clearCacheDirectoryIfConfigOrComposerLockfileChanged();
- $this->codebase = new Codebase(
- $config,
- $providers,
- $progress,
- );
+ $this->codebase = $codebase;
$this->stdout_report_options = $stdout_report_options;
$this->generated_report_options = $generated_report_options;
@@ -394,10 +378,12 @@ private function visitAutoloadFiles(): void
);
}
- public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void
+ public function serverMode(LanguageServer $server): void
{
+ $server->logInfo("Initializing: Visiting Autoload Files...");
$this->visitAutoloadFiles();
$this->codebase->diff_methods = true;
+ $server->logInfo("Initializing: Loading Reference Cache...");
$this->file_reference_provider->loadReferenceCache();
$this->codebase->enterServerMode();
@@ -418,103 +404,12 @@ public function server(?string $address = '127.0.0.1:12345', bool $socket_server
}
}
+ $server->logInfo("Initializing: Initialize Plugins...");
$this->config->initializePlugins($this);
foreach ($this->config->getProjectDirectories() as $dir_name) {
$this->checkDirWithConfig($dir_name, $this->config);
}
-
- @cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server');
-
- if (!$socket_server_mode && $address) {
- // Connect to a TCP server
- $socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
- if ($socket === false) {
- fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr");
- exit(1);
- }
- stream_set_blocking($socket, false);
- new LanguageServer(
- new ProtocolStreamReader($socket),
- new ProtocolStreamWriter($socket),
- $this,
- );
- Loop::run();
- } elseif ($socket_server_mode && $address) {
- // Run a TCP Server
- $tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
- if ($tcpServer === false) {
- fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr");
- exit(1);
- }
- fwrite(STDOUT, "Server listening on $address\n");
-
- $fork_available = true;
- if (!extension_loaded('pcntl')) {
- fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n");
- $fork_available = false;
- }
-
- $disabled_functions = array_map('trim', explode(',', ini_get('disable_functions')));
- if (in_array('pcntl_fork', $disabled_functions)) {
- fwrite(
- STDERR,
- "pcntl_fork() is disabled by php configuration (disable_functions directive)."
- . " Only a single connection will be accepted\n",
- );
- $fork_available = false;
- }
-
- while ($socket = stream_socket_accept($tcpServer, -1)) {
- fwrite(STDOUT, "Connection accepted\n");
- stream_set_blocking($socket, false);
- if ($fork_available) {
- // If PCNTL is available, fork a child process for the connection
- // An exit notification will only terminate the child process
- $pid = pcntl_fork();
- if ($pid === -1) {
- fwrite(STDERR, "Could not fork\n");
- exit(1);
- }
-
- if ($pid === 0) {
- // Child process
- $reader = new ProtocolStreamReader($socket);
- $reader->on(
- 'close',
- static function (): void {
- fwrite(STDOUT, "Connection closed\n");
- },
- );
- new LanguageServer(
- $reader,
- new ProtocolStreamWriter($socket),
- $this,
- );
- // Just for safety
- exit(0);
- }
- } else {
- // If PCNTL is not available, we only accept one connection.
- // An exit notification will terminate the server
- new LanguageServer(
- new ProtocolStreamReader($socket),
- new ProtocolStreamWriter($socket),
- $this,
- );
- Loop::run();
- }
- }
- } else {
- // Use STDIO
- stream_set_blocking(STDIN, false);
- new LanguageServer(
- new ProtocolStreamReader(STDIN),
- new ProtocolStreamWriter(STDOUT),
- $this,
- );
- Loop::run();
- }
}
/** @psalm-mutation-free */
diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php
index d9df6548301..429144b6808 100644
--- a/src/Psalm/Internal/Cli/LanguageServer.php
+++ b/src/Psalm/Internal/Cli/LanguageServer.php
@@ -2,20 +2,14 @@
namespace Psalm\Internal\Cli;
+use LanguageServerProtocol\MessageType;
use Psalm\Config;
-use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\CliUtils;
-use Psalm\Internal\Composer;
use Psalm\Internal\ErrorHandler;
use Psalm\Internal\Fork\PsalmRestarter;
use Psalm\Internal\IncludeCollector;
-use Psalm\Internal\Provider\ClassLikeStorageCacheProvider;
-use Psalm\Internal\Provider\FileProvider;
-use Psalm\Internal\Provider\FileReferenceCacheProvider;
-use Psalm\Internal\Provider\FileStorageCacheProvider;
-use Psalm\Internal\Provider\ParserCacheProvider;
-use Psalm\Internal\Provider\ProjectCacheProvider;
-use Psalm\Internal\Provider\Providers;
+use Psalm\Internal\LanguageServer\ClientConfiguration;
+use Psalm\Internal\LanguageServer\LanguageServer as LanguageServerLanguageServer;
use Psalm\Report;
use function array_key_exists;
@@ -52,16 +46,21 @@
require_once __DIR__ . '/../CliUtils.php';
require_once __DIR__ . '/../Composer.php';
require_once __DIR__ . '/../IncludeCollector.php';
+require_once __DIR__ . '/../LanguageServer/ClientConfiguration.php';
/**
* @internal
*/
final class LanguageServer
{
- /** @param array $argv */
+ /**
+ * @param array $argv
+ * @psalm-suppress ComplexMethod
+ */
public static function run(array $argv): void
{
CliUtils::checkRuntimeRequirements();
+ $clientConfiguration = new ClientConfiguration();
gc_disable();
ErrorHandler::install($argv);
$valid_short_options = [
@@ -72,7 +71,6 @@ public static function run(array $argv): void
];
$valid_long_options = [
- 'clear-cache',
'config:',
'find-dead-code',
'help',
@@ -82,7 +80,17 @@ public static function run(array $argv): void
'tcp:',
'tcp-server',
'disable-on-change::',
+ 'use-baseline:',
'enable-autocomplete::',
+ 'enable-code-actions::',
+ 'enable-provide-diagnostics::',
+ 'enable-provide-hover::',
+ 'enable-provide-signature-help::',
+ 'enable-provide-definition::',
+ 'show-diagnostic-warnings::',
+ 'in-memory::',
+ 'disable-xdebug::',
+ 'on-change-debounce-ms::',
'use-extended-diagnostic-codes',
'verbose',
];
@@ -164,12 +172,12 @@ static function (string $arg) use ($valid_long_options): void {
--find-dead-code
Look for dead code
- --clear-cache
- Clears all cache files that the language server uses for this specific project
-
--use-ini-defaults
Use PHP-provided ini defaults for memory and error display
+ --use-baseline=PATH
+ Allows you to use a baseline other than the default baseline provided in your config
+
--tcp=url
Use TCP mode (by default Psalm uses STDIO)
@@ -180,12 +188,39 @@ static function (string $arg) use ($valid_long_options): void {
If added, the language server will not respond to onChange events.
You can also specify a line count over which Psalm will not run on-change events.
+ --enable-code-actions[=BOOL]
+ Enables or disables code actions. Default is true.
+
+ --enable-provide-diagnostics[=BOOL]
+ Enables or disables providing diagnostics. Default is true.
+
--enable-autocomplete[=BOOL]
Enables or disables autocomplete on methods and properties. Default is true.
- --use-extended-diagnostic-codes
+ --enable-provide-hover[=BOOL]
+ Enables or disables providing hover. Default is true.
+
+ --enable-provide-signature-help[=BOOL]
+ Enables or disables providing signature help. Default is true.
+
+ --enable-provide-definition[=BOOL]
+ Enables or disables providing definition. Default is true.
+
+ --show-diagnostic-warnings[=BOOL]
+ Enables or disables showing diagnostic warnings. Default is true.
+
+ --use-extended-diagnostic-codes (DEPRECATED)
Enables sending help uri links with the code in diagnostic messages.
+ --on-change-debounce-ms=[INT]
+ The number of milliseconds to debounce onChange events.
+
+ --disable-xdebug[=BOOL]
+ Disable xdebug for performance reasons. Enable for debugging
+
+ --in-memory[=BOOL]
+ Use in-memory mode. Default is false. Experimental.
+
--verbose
Will send log messages to the client with information.
@@ -245,8 +280,14 @@ static function (string $arg) use ($valid_long_options): void {
'blackfire',
]);
- // If Xdebug is enabled, restart without it
- $ini_handler->check();
+ $disableXdebug = !isset($options['disable-xdebug'])
+ || !is_string($options['disable-xdebug'])
+ || strtolower($options['disable-xdebug']) !== 'false';
+
+ // If Xdebug is enabled, restart without it based on cli
+ if ($disableXdebug) {
+ $ini_handler->check();
+ }
setlocale(LC_CTYPE, 'C');
@@ -259,8 +300,6 @@ static function (string $arg) use ($valid_long_options): void {
}
}
- $find_unused_code = isset($options['find-dead-code']) ? 'auto' : null;
-
$config = CliUtils::initializeConfig(
$path_to_config,
$current_dir,
@@ -276,58 +315,85 @@ static function (string $arg) use ($valid_long_options): void {
$config->setServerMode();
- if (isset($options['clear-cache'])) {
+ $inMemory = isset($options['in-memory']) &&
+ is_string($options['in-memory']) &&
+ strtolower($options['in-memory']) === 'true';
+
+ if ($inMemory) {
+ $config->cache_directory = null;
+ } else {
$cache_directory = $config->getCacheDirectory();
if ($cache_directory !== null) {
Config::removeCacheDirectory($cache_directory);
}
- echo 'Cache directory deleted' . PHP_EOL;
- exit;
}
- $providers = new Providers(
- new FileProvider,
- new ParserCacheProvider($config),
- new FileStorageCacheProvider($config),
- new ClassLikeStorageCacheProvider($config),
- new FileReferenceCacheProvider($config),
- new ProjectCacheProvider(Composer::getLockFilePath($current_dir)),
- );
-
- $project_analyzer = new ProjectAnalyzer(
- $config,
- $providers,
- );
-
- if ($config->find_unused_variables) {
- $project_analyzer->getCodebase()->reportUnusedVariables();
+ if (isset($options['use-baseline']) && is_string($options['use-baseline'])) {
+ $clientConfiguration->baseline = $options['use-baseline'];
}
- if ($config->find_unused_code) {
- $find_unused_code = 'auto';
+ if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) {
+ $clientConfiguration->onchangeLineLimit = (int) $options['disable-on-change'];
}
- if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) {
- $project_analyzer->onchange_line_limit = (int) $options['disable-on-change'];
+ if (isset($options['on-change-debounce-ms']) && is_numeric($options['on-change-debounce-ms'])) {
+ $clientConfiguration->onChangeDebounceMs = (int) $options['on-change-debounce-ms'];
}
- $project_analyzer->provide_completion = !isset($options['enable-autocomplete'])
+ $clientConfiguration->provideDefinition = !isset($options['enable-provide-definition'])
+ || !is_string($options['enable-provide-definition'])
+ || strtolower($options['enable-provide-definition']) !== 'false';
+
+ $clientConfiguration->provideSignatureHelp = !isset($options['enable-provide-signature-help'])
+ || !is_string($options['enable-provide-signature-help'])
+ || strtolower($options['enable-provide-signature-help']) !== 'false';
+
+ $clientConfiguration->provideHover = !isset($options['enable-provide-hover'])
+ || !is_string($options['enable-provide-hover'])
+ || strtolower($options['enable-provide-hover']) !== 'false';
+
+ $clientConfiguration->provideDiagnostics = !isset($options['enable-provide-diagnostics'])
+ || !is_string($options['enable-provide-diagnostics'])
+ || strtolower($options['enable-provide-diagnostics']) !== 'false';
+
+ $clientConfiguration->provideCodeActions = !isset($options['enable-code-actions'])
+ || !is_string($options['enable-code-actions'])
+ || strtolower($options['enable-code-actions']) !== 'false';
+
+ $clientConfiguration->provideCompletion = !isset($options['enable-autocomplete'])
|| !is_string($options['enable-autocomplete'])
|| strtolower($options['enable-autocomplete']) !== 'false';
- if ($find_unused_code) {
- $project_analyzer->getCodebase()->reportUnusedCode($find_unused_code);
- }
+ $clientConfiguration->hideWarnings = !(
+ !isset($options['show-diagnostic-warnings'])
+ || !is_string($options['show-diagnostic-warnings'])
+ || strtolower($options['show-diagnostic-warnings']) !== 'false'
+ );
- if (isset($options['use-extended-diagnostic-codes'])) {
- $project_analyzer->language_server_use_extended_diagnostic_codes = true;
+ /**
+ * if ($config->find_unused_variables) {
+ * $project_analyzer->getCodebase()->reportUnusedVariables();
+ * }
+ */
+
+ $find_unused_code = isset($options['find-dead-code']) ? 'auto' : null;
+ if ($config->find_unused_code) {
+ $find_unused_code = 'auto';
+ }
+ if ($find_unused_code) {
+ $clientConfiguration->findUnusedCode = $find_unused_code;
}
if (isset($options['verbose'])) {
- $project_analyzer->language_server_verbose = true;
+ $clientConfiguration->logLevel = $options['verbose'] ? MessageType::LOG : MessageType::INFO;
+ } else {
+ $clientConfiguration->logLevel = MessageType::INFO;
}
- $project_analyzer->server($options['tcp'] ?? null, isset($options['tcp-server']));
+ $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null;
+ $clientConfiguration->TCPServerMode = isset($options['tcp-server']);
+
+ LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir, $inMemory);
}
}
diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php
index 2338465aa59..c7f4e378656 100644
--- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php
+++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php
@@ -4,15 +4,9 @@
namespace Psalm\Internal\LanguageServer\Client;
-use Amp\Promise;
-use Generator;
-use JsonMapper;
use LanguageServerProtocol\Diagnostic;
-use LanguageServerProtocol\TextDocumentIdentifier;
-use LanguageServerProtocol\TextDocumentItem;
use Psalm\Internal\LanguageServer\ClientHandler;
-
-use function Amp\call;
+use Psalm\Internal\LanguageServer\LanguageServer;
/**
* Provides method handlers for all textDocument/* methods
@@ -23,12 +17,12 @@ class TextDocument
{
private ClientHandler $handler;
- private JsonMapper $mapper;
+ private LanguageServer $server;
- public function __construct(ClientHandler $handler, JsonMapper $mapper)
+ public function __construct(ClientHandler $handler, LanguageServer $server)
{
$this->handler = $handler;
- $this->mapper = $mapper;
+ $this->server = $server;
}
/**
@@ -36,40 +30,18 @@ public function __construct(ClientHandler $handler, JsonMapper $mapper)
*
* @param Diagnostic[] $diagnostics
*/
- public function publishDiagnostics(string $uri, array $diagnostics): void
+ public function publishDiagnostics(string $uri, array $diagnostics, ?int $version = null): void
{
+ if (!$this->server->client->clientConfiguration->provideDiagnostics) {
+ return;
+ }
+
+ $this->server->logDebug("textDocument/publishDiagnostics");
+
$this->handler->notify('textDocument/publishDiagnostics', [
'uri' => $uri,
'diagnostics' => $diagnostics,
+ 'version' => $version,
]);
}
-
- /**
- * The content request is sent from a server to a client
- * to request the current content of a text document identified by the URI
- *
- * @param TextDocumentIdentifier $textDocument The document to get the content for
- * @return Promise The document's current content
- * @psalm-suppress MixedReturnTypeCoercion due to Psalm bug
- */
- public function xcontent(TextDocumentIdentifier $textDocument): Promise
- {
- return call(
- /**
- * @return Generator, object, TextDocumentItem>
- */
- function () use ($textDocument) {
- /** @var Promise