From 67c3726254413e5c4d7a3d5baa105b298d544bdb Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 1 Jul 2019 21:54:33 +0200 Subject: [PATCH] Implement signature help - closes #1841 (#1862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement signature help - closes #1841 * Add explicit signature count * [LSP] Do analysis for every signatureHelp request See here: https://github.com/vimeo/psalm/pull/1862#issuecomment-507007346 * Don’t language server information when checking property for init * Fix signature help for functions * Add try-catch around signature help Functions::getStorage() --- composer.json | 2 +- src/Psalm/Codebase.php | 54 +++++++ .../Expression/Call/ArgumentMapPopulator.php | 137 ++++++++++++++++++ .../Expression/Call/FunctionCallAnalyzer.php | 11 ++ .../Expression/Call/MethodCallAnalyzer.php | 16 ++ .../Expression/Call/NewAnalyzer.php | 11 ++ .../Expression/Call/StaticCallAnalyzer.php | 16 ++ src/Psalm/Internal/Codebase/Analyzer.php | 95 +++++++++++- .../LanguageServer/LanguageServer.php | 11 +- .../LanguageServer/Server/TextDocument.php | 52 +++++++ .../Provider/FileReferenceCacheProvider.php | 9 +- .../Provider/FileReferenceProvider.php | 9 +- .../FakeFileReferenceCacheProvider.php | 9 +- tests/LanguageServer/SymbolLookupTest.php | 83 +++++++++++ 14 files changed, 493 insertions(+), 22 deletions(-) create mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php diff --git a/composer.json b/composer.json index 97c1cf1a8cf..8ac370f963f 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 7b7d36dc8b0..5e41625c61b 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -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 */ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php new file mode 100644 index 00000000000..4ddc1f8c3dd --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php @@ -0,0 +1,137 @@ +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('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('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 + ); + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 677b4e56c7b..25fdc918bb8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -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 ) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 8bd0791b03a..1117a1b1db5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -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, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 166f40436d6..c00fc65fd9e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -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, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 673e717a9bd..a7218e79e82 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -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 diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index f68609b0c40..147aab7e766 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -61,7 +61,8 @@ * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * >, * class_locations: array>, @@ -151,6 +152,11 @@ class Analyzer */ private $type_map = []; + /** + * @var array> + */ + private $argument_map = []; + /** * @var array> */ @@ -434,9 +440,11 @@ function () { FileManipulationBuffer::add($file_path, $manipulations); } - foreach ($pool_data['file_maps'] as $file_path => list($reference_map, $type_map)) { + foreach ($pool_data['file_maps'] as $file_path => $file_maps) { + list($reference_map, $type_map, $argument_map) = $file_maps; $this->reference_map[$file_path] = $reference_map; $this->type_map[$file_path] = $type_map; + $this->argument_map[$file_path] = $argument_map; } } @@ -520,9 +528,10 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer) $this->existing_issues = $codebase->file_reference_provider->getExistingIssues(); $file_maps = $codebase->file_reference_provider->getFileMaps(); - foreach ($file_maps as $file_path => list($reference_map, $type_map)) { + foreach ($file_maps as $file_path => list($reference_map, $type_map, $argument_map)) { $this->reference_map[$file_path] = $reference_map; $this->type_map[$file_path] = $type_map; + $this->argument_map[$file_path] = $argument_map; } } @@ -879,6 +888,40 @@ public function shiftFileOffsets(array $diff_map) } } } + + foreach ($this->argument_map as $file_path => &$argument_map) { + if (!isset($this->analyzed_methods[$file_path])) { + unset($this->argument_map[$file_path]); + continue; + } + + $file_diff_map = $diff_map[$file_path] ?? []; + + if (!$file_diff_map) { + continue; + } + + $first_diff_offset = $file_diff_map[0][0]; + $last_diff_offset = $file_diff_map[count($file_diff_map) - 1][1]; + + foreach ($argument_map as $argument_from => list($argument_to, $method_id, $argument_number)) { + if ($argument_to < $first_diff_offset || $argument_from > $last_diff_offset) { + continue; + } + + + foreach ($file_diff_map as list($from, $to, $file_offset)) { + if ($argument_from >= $from && $argument_from <= $to) { + unset($argument_map[$argument_from]); + $argument_map[$argument_from += $file_offset] = [ + $argument_to += $file_offset, + $method_id, + $argument_number, + ]; + } + } + } + } } /** @@ -1002,6 +1045,20 @@ public function addNodeType( ]; } + public function addNodeArgument( + string $file_path, + int $start_position, + int $end_position, + string $reference, + int $argument_number + ): void { + $this->argument_map[$file_path][$start_position] = [ + $end_position, + $reference, + $argument_number + ]; + } + /** * @return void */ @@ -1255,6 +1312,14 @@ public function removeExistingDataForFile($file_path, $start, $end) } } } + + if (isset($this->argument_map[$file_path])) { + foreach ($this->argument_map[$file_path] as $map_start => $_) { + if ($map_start >= $start && $map_start <= $end) { + unset($this->argument_map[$file_path][$map_start]); + } + } + } } /** @@ -1270,7 +1335,8 @@ public function getAnalyzedMethods() * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * > */ @@ -1279,14 +1345,22 @@ public function getFileMaps() $file_maps = []; foreach ($this->reference_map as $file_path => $reference_map) { - $file_maps[$file_path] = [$reference_map, []]; + $file_maps[$file_path] = [$reference_map, [], []]; } foreach ($this->type_map as $file_path => $type_map) { if (isset($file_maps[$file_path])) { $file_maps[$file_path][1] = $type_map; } else { - $file_maps[$file_path] = [[], $type_map]; + $file_maps[$file_path] = [[], $type_map, []]; + } + } + + foreach ($this->argument_map as $file_path => $argument_map) { + if (isset($file_maps[$file_path])) { + $file_maps[$file_path][2] = $argument_map; + } else { + $file_maps[$file_path] = [[], [], $argument_map]; } } @@ -1294,13 +1368,18 @@ public function getFileMaps() } /** - * @return array{0: TaggedCodeType, 1: TaggedCodeType} + * @return array{ + * 0: TaggedCodeType, + * 1: TaggedCodeType, + * 2: array + * } */ public function getMapsForFile(string $file_path) { return [ $this->reference_map[$file_path] ?? [], - $this->type_map[$file_path] ?? [] + $this->type_map[$file_path] ?? [], + $this->argument_map[$file_path] ?? [], ]; } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 0ad2e53ae1e..def5edeb524 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -17,6 +17,7 @@ use LanguageServerProtocol\{Range, Position, Diagnostic, DiagnosticSeverity}; use AdvancedJsonRpc; use Amp\Promise; +use Symfony\Component\VarExporter\VarExporter; use Throwable; use function Amp\call; use function Amp\asyncCoroutine; @@ -122,6 +123,11 @@ function (Message $msg) { return; } + /** @psalm-suppress UndefinedPropertyFetch */ + if ($msg->body->method === 'textDocument/signatureHelp') { + $this->doAnalysis(); + } + $result = null; $error = null; try { @@ -241,10 +247,7 @@ function () use ($capabilities, $rootPath, $processId) { $serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':']; } - /* - $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(); - $serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ',']; - */ + $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']); // Support global references $serverCapabilities->xworkspaceReferencesProvider = false; diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index ae8e000ca83..dbc5661da12 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -37,6 +37,8 @@ use function error_log; use function count; use function substr_count; +use function strlen; +use function strpos; /** * Provides method handlers for all textDocument/* methods @@ -273,4 +275,54 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit return new Success(new CompletionList($completion_items, false)); } + + public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise + { + $file_path = LanguageServer::uriToPath($textDocument->uri); + + $argument_location = $this->codebase->getFunctionArgumentAtPosition($file_path, $position); + if ($argument_location === null) { + return new Success(new \LanguageServerProtocol\SignatureHelp()); + } + + list($function_symbol, $argument_number) = $argument_location; + if (strpos($function_symbol, '::') !== false) { + $declaring_method_id = $this->codebase->methods->getDeclaringMethodId($function_symbol); + if ($declaring_method_id === null) { + return new Success(new \LanguageServerProtocol\SignatureHelp()); + } + $method_storage = $this->codebase->methods->getStorage($declaring_method_id); + $params = $method_storage->params; + } else { + try { + $function_storage = $this->codebase->functions->getStorage(null, $function_symbol); + } catch (\Exception $exception) { + return new Success(new \LanguageServerProtocol\SignatureHelp()); + } + $params = $function_storage->params; + } + + $signature_label = '('; + $parameters = []; + foreach ($params as $i => $param) { + $parameter_label = ($param->type ?: 'mixed') . ' $' . $param->name; + $parameters[] = new \LanguageServerProtocol\ParameterInformation([ + strlen($signature_label), + strlen($signature_label) + strlen($parameter_label), + ]) ; + $signature_label .= $parameter_label; + + if ($i < (count($params) - 1)) { + $signature_label .= ', '; + } + } + $signature_label .= ')'; + + return new Success(new \LanguageServerProtocol\SignatureHelp([ + new \LanguageServerProtocol\SignatureInformation( + $signature_label, + $parameters + ), + ], 0, $argument_number)); + } } diff --git a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php index be1ba7f86d5..e63dd78d76b 100644 --- a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php @@ -519,7 +519,8 @@ public function setAnalyzedMethodCache(array $analyzed_methods) * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * >|false */ @@ -538,7 +539,8 @@ public function getFileMapCache() * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * > */ @@ -554,7 +556,8 @@ public function getFileMapCache() * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * > $file_maps * @return void diff --git a/src/Psalm/Internal/Provider/FileReferenceProvider.php b/src/Psalm/Internal/Provider/FileReferenceProvider.php index a1ee9719664..abe04c0e0ab 100644 --- a/src/Psalm/Internal/Provider/FileReferenceProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceProvider.php @@ -134,7 +134,8 @@ class FileReferenceProvider * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * > */ @@ -939,7 +940,8 @@ public function setAnalyzedMethods(array $analyzed_methods) * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * > $file_maps */ @@ -978,7 +980,8 @@ public function getAnalyzedMethods() * string, * array{ * 0: TaggedCodeType, - * 1: TaggedCodeType + * 1: TaggedCodeType, + * 2: array * } * > */ diff --git a/tests/Internal/Provider/FakeFileReferenceCacheProvider.php b/tests/Internal/Provider/FakeFileReferenceCacheProvider.php index 39b42275585..02447c6a7d5 100644 --- a/tests/Internal/Provider/FakeFileReferenceCacheProvider.php +++ b/tests/Internal/Provider/FakeFileReferenceCacheProvider.php @@ -42,7 +42,8 @@ class FakeFileReferenceCacheProvider extends \Psalm\Internal\Provider\FileRefere * string, * array{ * 0: array, - * 1: array + * 1: array, + * 2: array * } * > */ @@ -247,7 +248,8 @@ public function setAnalyzedMethodCache(array $correct_methods) * string, * array{ * 0: array, - * 1: array + * 1: array, + * 2: array * } * > */ @@ -261,7 +263,8 @@ public function getFileMapCache() * string, * array{ * 0: array, - * 1: array + * 1: array, + * 2: array * } * > $file_maps * diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index 3c853bd4774..8b86107893b 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -295,4 +295,87 @@ class A { $this->assertSame('Exception', $symbol_at_position[0]); } + + /** + * @return array + */ + public function providerGetSignatureHelp(): array + { + return [ + [new Position(5, 34), null, null], + [new Position(5, 35), 'B\A::foo', 0], + [new Position(5, 36), null, null], + [new Position(6, 34), null, null], + [new Position(6, 35), 'B\A::foo', 0], + [new Position(6, 40), 'B\A::foo', 0], + [new Position(6, 41), 'B\A::foo', 1], + [new Position(6, 47), 'B\A::foo', 1], + [new Position(6, 48), null, null], + [new Position(7, 40), 'B\A::foo', 0], + [new Position(7, 41), 'B\A::foo', 1], + [new Position(7, 42), 'B\A::foo', 1], + [new Position(8, 40), 'B\A::foo', 0], + [new Position(8, 46), 'B\A::bar', 0], + [new Position(8, 47), 'B\A::foo', 0], + [new Position(10, 40), 'B\A::staticfoo', 0], + [new Position(12, 28), 'B\foo', 0], + [new Position(14, 30), 'B\A::__construct', 0], + ]; + } + + /** + * @dataProvider providerGetSignatureHelp + */ + public function testGetSignatureHelp( + Position $position, + ?string $expected_symbol, + ?int $expected_argument_number + ): void { + $codebase = $this->project_analyzer->getCodebase(); + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'foo(); + $this->foo("Foo", "Bar"); + $this->foo("Foo", ); + $this->foo($this->bar()); + + self::staticFoo(); + + foo(); + + new A(); + + // Blocked by https://github.com/nikic/PHP-Parser/issues/616 + //$this->foo(, "Bar"); + //$this->foo(,,); + } + + public function bar(string $a) {} + + public static function staticFoo(string $a) {} + + public function __construct() {} + } + + function foo(string $a) { + }' + ); + + $codebase->file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + $this->analyzeFile('somefile.php', new Context()); + + $reference_location = $codebase->getFunctionArgumentAtPosition('somefile.php', $position); + list($symbol, $argument_number) = $reference_location; + $this->assertSame($expected_symbol, $symbol); + $this->assertSame($expected_argument_number, $argument_number); + } }