diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index cf38b84a..bfc59245 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -182,8 +182,12 @@ class Script(object): :rtype: list of :class:`classes.Definition` """ - c = helpers.ContextResults(self._evaluator, self.source, self._get_module(), self._pos) - definitions = c.get_results() + leaf = self._get_module().name_for_position(self._pos) + if leaf is None: + leaf = self._get_module().get_leaf_for_position(self._pos) + if leaf is None: + return [] + definitions = helpers.evaluate_goto_definition(self._evaluator, leaf) names = [s.name for s in definitions] defs = [classes.Definition(self._evaluator, name) for name in names] @@ -272,26 +276,30 @@ class Script(object): abs()# <-- cursor is here - This would return ``None``. + This would return an empty list.. :rtype: list of :class:`classes.CallSignature` """ - call_txt, call_index, key_name, start_pos = self._user_context.call_signature() - if call_txt is None: + call_signature_details = \ + helpers.get_call_signature_details(self._get_module(), self._pos) + if call_signature_details is None: return [] - stmt = inference.get_under_cursor_stmt(self._evaluator, self._parser, - call_txt, start_pos) - if stmt is None: - return [] - - with common.scale_speed_settings(settings.scale_call_signatures): - origins = cache.cache_call_signatures(self._evaluator, stmt, - self.source, self._pos) + # TODO insert caching again here. + #with common.scale_speed_settings(settings.scale_call_signatures): + # definitions = cache.cache_call_signatures(self._evaluator, stmt, + # self.source, self._pos) + definitions = helpers.evaluate_goto_definition( + self._evaluator, + call_signature_details.leaf + ) debug.speed('func_call followed') - return [classes.CallSignature(self._evaluator, o.name, stmt, call_index, key_name) - for o in origins if hasattr(o, 'py__call__')] + return [classes.CallSignature(self._evaluator, d.name, + call_signature_details.leaf.start_pos, + call_signature_details.call_index, + call_signature_details.keyword_name) + for d in definitions if hasattr(d, 'py__call__')] def _analysis(self): self._evaluator.is_analysis = True diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 0eb24664..367eaea4 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -629,11 +629,11 @@ class CallSignature(Definition): It knows what functions you are currently in. e.g. `isinstance(` would return the `isinstance` function. without `(` it would return nothing. """ - def __init__(self, evaluator, executable_name, call_stmt, index, key_name): + def __init__(self, evaluator, executable_name, bracket_start_pos, index, key_name): super(CallSignature, self).__init__(evaluator, executable_name) self._index = index self._key_name = key_name - self._call_stmt = call_stmt + self._bracket_start_pos = bracket_start_pos @property def index(self): @@ -665,7 +665,7 @@ class CallSignature(Definition): The indent of the bracket that is responsible for the last function call. """ - return self._call_stmt.end_pos + return self._bracket_start_pos @property def call_name(self): diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index 6d789fe9..70ec8bdb 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -6,6 +6,7 @@ from collections import namedtuple from jedi import common from jedi.evaluate import imports +from jedi.evaluate.helpers import deep_ast_copy from jedi import parser from jedi.parser import tokenize, token @@ -169,68 +170,63 @@ def get_possible_completion_types(grammar, stack): return keywords, grammar_labels -class ContextResults(): - def __init__(self, evaluator, source, module, pos): - self._evaluator = evaluator - self._module = module - self._source = source - self._pos = pos +def evaluate_goto_definition(evaluator, leaf): + if leaf.type == 'name': + # In case of a name we can just use goto_definition which does all the + # magic itself. + return evaluator.goto_definitions(leaf) - def _on_defining_name(self, leaf): - return [self._evaluator.wrap(self._parser.user_scope())] + node = None + parent = leaf.parent + if parent.type == 'atom': + node = leaf.parent + elif parent.type == 'trailer': + index = parent.parent.children.index(parent) + node = deep_ast_copy(parent.parent) + node.children = node.children[:index + 1] - def get_results(self): - ''' - try: - stack = get_stack_at_position(self._evaluator.grammar, self._source, self._module, self._leaf.end_pos) - except OnErrorLeaf: - return [] -''' - - name = self._module.name_for_position(self._pos) - if name is not None: - return self._evaluator.goto_definitions(name) - - leaf = self._module.get_leaf_for_position(self._pos) - if leaf is None: - return [] - - if leaf.parent.type == 'atom': - return self._evaluator.eval_element(leaf.parent) - if leaf.parent.type == 'trailer': - return self._evaluator.eval_element(leaf.parent.parent) + if node is None: return [] - symbol_names = list(stack.get_node_names(self._evaluator.grammar)) - - nodes = list(stack.get_nodes()) - - if "import_stmt" in symbol_names: - level = 0 - only_modules = True - level, names = self._parse_dotted_names(nodes) - if "import_from" in symbol_names: - if 'import' in nodes: - only_modules = False - else: - assert "import_name" in symbol_names - - completion_names += self._get_importer_names( - names, - level, - only_modules - ) - elif nodes[-2] in ('as', 'def', 'class'): - # No completions for ``with x as foo`` and ``import x as foo``. - # Also true for defining names as a class or function. - return self._on_defining_name(self._leaf) - else: - completion_names += self._simple_complete(completion_parts) - return + return evaluator.eval_element(node) -class GotoDefinition(ContextResults): - def _(): - definitions = inference.type_inference( - self._evaluator, self._parser, self._user_context, - self._pos, goto_path - ) +CallSignatureDetails = namedtuple( + 'CallSignatureDetails', + ['leaf', 'call_index', 'keyword_name'] +) + + +def _get_call_signature_details_from_error_node(node, position): + for index, element in reversed(list(enumerate(node.children))): + # `index > 0` means that it's a trailer and not an atom. + if element == '(' and element.end_pos <= position and index > 0: + name = element.get_previous_leaf() + if name.type == 'name': + nodes_before = [c for c in node.children[index:] if c.start_pos < position] + return CallSignatureDetails(name, nodes_before.count(','), None) + + +def get_call_signature_details(module, position): + leaf = module.get_leaf_for_position(position, include_prefixes=True) + if leaf == ')': + if leaf.end_pos == position: + leaf = leaf.get_next_leaf() + # Now that we know where we are in the syntax tree, we start to look at + # parents for possible function definitions. + node = leaf.parent + name = None + while node is not None: + for n in node.children: + if n.start_pos < position and n.type == 'error_node': + result = _get_call_signature_details_from_error_node(n, position) + if result is not None: + return result + + if node.type == 'trailer' and node.children[0] == '(': + name = node.get_previous_sibling() + nodes_before = [c for c in node.children if c.start_pos < position] + return CallSignatureDetails(name, nodes_before.count(','), None) + + node = node.parent + + return None diff --git a/jedi/evaluate/helpers.py b/jedi/evaluate/helpers.py index ca00d9b9..8995adb1 100644 --- a/jedi/evaluate/helpers.py +++ b/jedi/evaluate/helpers.py @@ -81,15 +81,32 @@ def call_of_name(name, cut_own_trailer=False): # TODO remove cut_own_trailer option, since its always used with it. Just # ignore it, It's not what we want anyway. Or document it better? """ - par = name - if tree.is_node(par.parent, 'trailer'): + trailer = name.parent + if trailer.type != 'trailer' or trailer.children[0] != '.': + return name + + assert not cut_own_trailer # TODO remove + power = trailer.parent + index = power.children.index(trailer) + power = deep_ast_copy(power) + power.children[index + 1:] = [] + + if power.type == 'error_node': + transformed = tree.Node('power', power.children) + transformed.parent = power.parent + return transformed + + return power + if 1: par = par.parent if par.children[0] in ('(', '['): # The trailer is not a NAME.NAME trailer, but a call to something. return name power = par.parent - if tree.is_node(power, 'power', 'atom_expr') \ + # `atom_expr` got introduced in Python 3.5 and is essentially just the + # whole call part without the optional ** power element. + if power.type in ('power', 'atom_expr') \ and power.children[0] != name \ and not (power.children[-2] == '**' and name.start_pos > power.children[-1].start_pos): diff --git a/test/completion/classes.py b/test/completion/classes.py index 0b1f72b3..35b0619c 100644 --- a/test/completion/classes.py +++ b/test/completion/classes.py @@ -83,6 +83,9 @@ TestClass.var_local. #? int() TestClass().ret(1) +# Should not return int(), because we want the type before `.ret(1)`. +#? 11 TestClass() +TestClass().ret(1) #? int() inst.ret(1) diff --git a/test/test_api/test_call_signatures.py b/test/test_api/test_call_signatures.py index aefd3f85..a0758112 100644 --- a/test/test_api/test_call_signatures.py +++ b/test/test_api/test_call_signatures.py @@ -14,7 +14,8 @@ class TestCallSignatures(TestCase): assert len(signatures) <= 1 if not signatures: - assert expected_name is None + assert expected_name is None, \ + 'There are no signatures, but %s expected.' % expected_name else: assert signatures[0].name == expected_name assert signatures[0].index == expected_index @@ -27,9 +28,6 @@ class TestCallSignatures(TestCase): def test_simple(self): run = self._run_simple - s7 = "str().upper().center(" - s8 = "str(int[zip(" - run(s7, 'center', 0) # simple s1 = "sorted(a, str("