Skip to content

Commit

Permalink
Implement signature help - closes vimeo#1841
Browse files Browse the repository at this point in the history
  • Loading branch information
iluuu1994 committed Jul 1, 2019
1 parent e876feb commit eb7bc1b
Show file tree
Hide file tree
Showing 14 changed files with 466 additions and 22 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
54 changes: 54 additions & 0 deletions src/Psalm/Codebase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ public static function analyze(
strtolower($function_id)
);

ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$function_id
);

if (!$namespaced_function_exists
&& !$stmt->name instanceof PhpParser\Node\Name\FullyQualified
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -971,6 +976,13 @@ function (PhpParser\Node\Arg $arg) {
}
}

ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$method_id
);

if (self::checkMethodArgs(
$method_id,
$args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,13 @@ public static function analyze(
)) {
$method_id = $fq_class_name . '::__construct';

ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$method_id
);

if (self::checkMethodArgs(
$method_id,
$stmt->args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -345,6 +350,13 @@ public static function analyze(
$method_name_lc = strtolower($stmt->name->name);
$method_id = $fq_class_name . '::' . $method_name_lc;

ArgumentMapPopulator::recordArgumentPositions(
$statements_analyzer,
$stmt,
$codebase,
$method_id
);

$args = $stmt->args;

if ($intersection_types
Expand Down
Loading

0 comments on commit eb7bc1b

Please sign in to comment.