Skip to content
Permalink
Browse files

Implement signature help - closes #1841 (#1862)

* Implement signature help - closes #1841

* Add explicit signature count

* [LSP] Do analysis for every signatureHelp request

See here: #1862 (comment)

* Don’t language server information when checking property for init

* Fix signature help for functions

* Add try-catch around signature help Functions::getStorage()
  • Loading branch information...
iluuu1994 authored and muglug committed Jul 1, 2019
1 parent e876feb commit 67c3726254413e5c4d7a3d5baa105b298d544bdb
@@ -15,7 +15,7 @@
"openlss/lib-array2xml": "^1.0",
"ocramius/package-versions": "^1.2",
"composer/xdebug-handler": "^1.1",
"felixfbecker/language-server-protocol": "^1.3",
"felixfbecker/language-server-protocol": "^1.4",
"felixfbecker/advanced-json-rpc": "^3.0.3",
"netresearch/jsonmapper": "^1.0",
"webmozart/glob": "^4.1",
@@ -1142,6 +1142,60 @@ public function getReferenceAtPosition(string $file_path, Position $position)
return [$reference, $range];
}
/**
* @return array{0: string, 1: int, 2: Range}|null
*/
public function getFunctionArgumentAtPosition(string $file_path, Position $position)
{
$is_open = $this->file_provider->isOpen($file_path);
if (!$is_open) {
throw new \Psalm\Exception\UnanalyzedFileException($file_path . ' is not open');
}
$file_contents = $this->getFileContents($file_path);
$offset = $position->toOffset($file_contents);
list(,, $argument_map) = $this->analyzer->getMapsForFile($file_path);
$reference = null;
$argument_number = null;
if (!$argument_map) {
return null;
}
$start_pos = null;
$end_pos = null;
ksort($argument_map);
foreach ($argument_map as $start_pos => list($end_pos, $possible_reference, $possible_argument_number)) {
if ($offset < $start_pos) {
break;
}
if ($offset > $end_pos) {
continue;
}
$reference = $possible_reference;
$argument_number = $possible_argument_number;
}
if ($reference === null || $start_pos === null || $end_pos === null || $argument_number === null) {
return null;
}
$range = new Range(
self::getPositionFromOffset($start_pos, $file_contents),
self::getPositionFromOffset($end_pos, $file_contents)
);
return [$reference, $argument_number, $range];
}
/**
* @return array{0: string, 1: '->'|'::'|'symbol', 2: int}|null
*/
@@ -0,0 +1,137 @@
<?php
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use function substr;
use function token_get_all;
use function array_reverse;
use function is_string;
use function strlen;
use function array_shift;
use function reset;
class ArgumentMapPopulator
{
/**
* @param MethodCall|StaticCall|FuncCall|New_ $stmt
*/
public static function recordArgumentPositions(
StatementsAnalyzer $statements_analyzer,
Expr $stmt,
Codebase $codebase,
string $function_reference
): void {
$file_content = $codebase->file_provider->getContents($statements_analyzer->getFilePath());
// Find opening paren
$first_argument = $stmt->args[0] ?? null;
$first_argument_character = $first_argument !== null
? $first_argument->getStartFilePos()
: $stmt->getEndFilePos();
$method_name_and_first_paren_source_code_length = $first_argument_character - $stmt->getStartFilePos();
// FIXME: There are weird ::__construct calls in the AST for `extends`
if ($method_name_and_first_paren_source_code_length <= 0) {
return;
}
$method_name_and_first_paren_source_code = substr(
$file_content,
$stmt->getStartFilePos(),
$method_name_and_first_paren_source_code_length
);
$method_name_and_first_paren_tokens = token_get_all('<?php ' . $method_name_and_first_paren_source_code);
$opening_paren_position = $first_argument_character;
foreach (array_reverse($method_name_and_first_paren_tokens) as $token) {
$token = is_string($token) ? $token : $token[1];
$opening_paren_position -= strlen($token);
if ($token === '(') {
break;
}
}
// New instances can be created without parens
if ($opening_paren_position < $stmt->getStartFilePos()) {
return;
}
// Record ranges of the source code that need to be tokenized to find commas
/** @var array{0: int, 1: int}[] $ranges */
$ranges = [];
// Add range between opening paren and first argument
$first_argument = $stmt->args[0] ?? null;
$first_argument_starting_position = $first_argument !== null
? $first_argument->getStartFilePos()
: $stmt->getEndFilePos();
$first_range_starting_position = $opening_paren_position + 1;
if ($first_range_starting_position !== $first_argument_starting_position) {
$ranges[] = [$first_range_starting_position, $first_argument_starting_position];
}
// Add range between arguments
foreach ($stmt->args as $i => $argument) {
$range_start = $argument->getEndFilePos() + 1;
$next_argument = $stmt->args[$i + 1] ?? null;
$range_end = $next_argument !== null
? $next_argument->getStartFilePos()
: $stmt->getEndFilePos();
if ($range_start !== $range_end) {
$ranges[] = [$range_start, $range_end];
}
}
$commas = [];
foreach ($ranges as $range) {
$position = $range[0];
$length = $range[1] - $position;
$range_source_code = substr($file_content, $position, $length);
$range_tokens = token_get_all('<?php ' . $range_source_code);
array_shift($range_tokens);
$current_position = $position;
foreach ($range_tokens as $token) {
$token = is_string($token) ? $token : $token[1];
if ($token === ',') {
$commas[] = $current_position;
}
$current_position += strlen($token);
}
}
$argument_start_position = $opening_paren_position + 1;
$argument_number = 0;
while (!empty($commas)) {
$comma = reset($commas);
array_shift($commas);
$codebase->analyzer->addNodeArgument(
$statements_analyzer->getFilePath(),
$argument_start_position,
$comma,
$function_reference,
$argument_number
);
++$argument_number;
$argument_start_position = $comma + 1;
}
$codebase->analyzer->addNodeArgument(
$statements_analyzer->getFilePath(),
$argument_start_position,
$stmt->getEndFilePos(),
$function_reference,
$argument_number
);
}
}
@@ -236,6 +236,17 @@ public static function analyze(
strtolower($function_id)
);
if (!$context->collect_initializations
&& !$context->collect_mutations
) {
ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$function_id
);
}
if (!$namespaced_function_exists
&& !$stmt->name instanceof PhpParser\Node\Name\FullyQualified
) {
@@ -46,6 +46,11 @@
use function array_search;
use function array_keys;
use function in_array;
use function substr;
use function token_get_all;
use function array_reverse;
use function strlen;
use function reset;
/**
* @internal
@@ -971,6 +976,17 @@ function (PhpParser\Node\Arg $arg) {
}
}
if (!$context->collect_initializations
&& !$context->collect_mutations
) {
ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$method_id
);
}
if (self::checkMethodArgs(
$method_id,
$args,
@@ -371,6 +371,17 @@ public static function analyze(
)) {
$method_id = $fq_class_name . '::__construct';
if (!$context->collect_initializations
&& !$context->collect_mutations
) {
ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$method_id
);
}
if (self::checkMethodArgs(
$method_id,
$stmt->args,
@@ -2,6 +2,7 @@
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
use PhpParser;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\MethodAnalyzer;
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
@@ -30,6 +31,10 @@
use function is_string;
use function strlen;
use function substr;
use function token_get_all;
use function array_reverse;
use function array_shift;
use function reset;
/**
* @internal
@@ -345,6 +350,17 @@ public static function analyze(
$method_name_lc = strtolower($stmt->name->name);
$method_id = $fq_class_name . '::' . $method_name_lc;
if (!$context->collect_initializations
&& !$context->collect_mutations
) {
ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$method_id
);
}
$args = $stmt->args;
if ($intersection_types

0 comments on commit 67c3726

Please sign in to comment.
You can’t perform that action at this time.