diff --git a/jedi/inference/value/dynamic_arrays.py b/jedi/inference/value/dynamic_arrays.py new file mode 100644 index 00000000..229a5dfb --- /dev/null +++ b/jedi/inference/value/dynamic_arrays.py @@ -0,0 +1,164 @@ +""" +A module to deal with stuff like `list.append` and `set.add`. + +Array modifications +******************* + +If the content of an array (``set``/``list``) is requested somewhere, the +current module will be checked for appearances of ``arr.append``, +``arr.insert``, etc. If the ``arr`` name points to an actual array, the +content will be added + +This can be really cpu intensive, as you can imagine. Because |jedi| has to +follow **every** ``append`` and check wheter it's the right array. However this +works pretty good, because in *slow* cases, the recursion detector and other +settings will stop this process. + +It is important to note that: + +1. Array modfications work only in the current module. +2. Jedi only checks Array additions; ``list.pop``, etc are ignored. +""" +from jedi import debug +from jedi import settings +from jedi.inference import recursion +from jedi.inference.base_value import ValueSet, NO_VALUES, HelperValueMixin +from jedi.inference.helpers import infer_call_of_leaf +from jedi.inference.cache import inference_state_method_cache + + +def check_array_additions(context, sequence): + """ Just a mapper function for the internal _internal_check_array_additions """ + if sequence.array_type not in ('list', 'set'): + # TODO also check for dict updates + return NO_VALUES + + return _internal_check_array_additions(context, sequence) + + +@inference_state_method_cache(default=NO_VALUES) +@debug.increase_indent +def _internal_check_array_additions(context, sequence): + """ + Checks if a `Array` has "add" (append, insert, extend) statements: + + >>> a = [""] + >>> a.append(1) + """ + from jedi.inference import arguments + + debug.dbg('Dynamic array search for %s' % sequence, color='MAGENTA') + module_context = context.get_root_context() + if not settings.dynamic_array_additions or module_context.is_compiled(): + debug.dbg('Dynamic array search aborted.', color='MAGENTA') + return NO_VALUES + + def find_additions(context, arglist, add_name): + params = list(arguments.TreeArguments(context.inference_state, context, arglist).unpack()) + result = set() + if add_name in ['insert']: + params = params[1:] + if add_name in ['append', 'add', 'insert']: + for key, lazy_value in params: + result.add(lazy_value) + elif add_name in ['extend', 'update']: + for key, lazy_value in params: + result |= set(lazy_value.infer().iterate()) + return result + + temp_param_add, settings.dynamic_params_for_other_modules = \ + settings.dynamic_params_for_other_modules, False + + is_list = sequence.name.string_name == 'list' + search_names = (['append', 'extend', 'insert'] if is_list else ['add', 'update']) + + added_types = set() + for add_name in search_names: + try: + possible_names = module_context.tree_node.get_used_names()[add_name] + except KeyError: + continue + else: + for name in possible_names: + value_node = context.tree_node + if not (value_node.start_pos < name.start_pos < value_node.end_pos): + continue + trailer = name.parent + power = trailer.parent + trailer_pos = power.children.index(trailer) + try: + execution_trailer = power.children[trailer_pos + 1] + except IndexError: + continue + else: + if execution_trailer.type != 'trailer' \ + or execution_trailer.children[0] != '(' \ + or execution_trailer.children[1] == ')': + continue + + random_context = context.create_context(name) + + with recursion.execution_allowed(context.inference_state, power) as allowed: + if allowed: + found = infer_call_of_leaf( + random_context, + name, + cut_own_trailer=True + ) + if sequence in found: + # The arrays match. Now add the results + added_types |= find_additions( + random_context, + execution_trailer.children[1], + add_name + ) + + # reset settings + settings.dynamic_params_for_other_modules = temp_param_add + debug.dbg('Dynamic array result %s' % added_types, color='MAGENTA') + return added_types + + +def get_dynamic_array_instance(instance, arguments): + """Used for set() and list() instances.""" + ai = _ArrayInstance(instance, arguments) + from jedi.inference import arguments + return arguments.ValuesArguments([ValueSet([ai])]) + + +class _ArrayInstance(HelperValueMixin): + """ + Used for the usage of set() and list(). + This is definitely a hack, but a good one :-) + It makes it possible to use set/list conversions. + + This is not a proper context, because it doesn't have to be. It's not used + in the wild, it's just used within typeshed as an argument to `__init__` + for set/list and never used in any other place. + """ + def __init__(self, instance, var_args): + self.instance = instance + self.var_args = var_args + + def py__class__(self): + tuple_, = self.instance.inference_state.builtins_module.py__getattribute__('tuple') + return tuple_ + + def py__iter__(self, contextualized_node=None): + var_args = self.var_args + try: + _, lazy_value = next(var_args.unpack()) + except StopIteration: + pass + else: + for lazy in lazy_value.infer().iterate(): + yield lazy + + from jedi.inference import arguments + if isinstance(var_args, arguments.TreeArguments): + additions = _internal_check_array_additions(var_args.context, self.instance) + for addition in additions: + yield addition + + def iterate(self, contextualized_node=None, is_async=False): + return self.py__iter__(contextualized_node) diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index ede5a35b..b463c388 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -18,6 +18,7 @@ from jedi.inference.value.function import \ from jedi.inference.value.klass import ClassValue, apply_py__get__, \ ClassFilter from jedi.inference.value import iterable +from jedi.inference.value.dynamic_arrays import get_dynamic_array_instance from jedi.parser_utils import get_parent_scope @@ -263,7 +264,7 @@ class TreeInstance(AbstractInstanceValue): and parent_context.get_root_context().is_builtins_module(): # compare the module path with the builtin name. if settings.dynamic_array_additions: - var_args = iterable.get_dynamic_array_instance(self, var_args) + var_args = get_dynamic_array_instance(self, var_args) super(TreeInstance, self).__init__(inference_state, parent_context, class_value, var_args) diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index 57d8030e..637135c6 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -1,45 +1,25 @@ """ Contains all classes and functions to deal with lists, dicts, generators and iterators in general. - -Array modifications -******************* - -If the content of an array (``set``/``list``) is requested somewhere, the -current module will be checked for appearances of ``arr.append``, -``arr.insert``, etc. If the ``arr`` name points to an actual array, the -content will be added - -This can be really cpu intensive, as you can imagine. Because |jedi| has to -follow **every** ``append`` and check wheter it's the right array. However this -works pretty good, because in *slow* cases, the recursion detector and other -settings will stop this process. - -It is important to note that: - -1. Array modfications work only in the current module. -2. Jedi only checks Array additions; ``list.pop``, etc are ignored. """ import sys -from jedi import debug -from jedi import settings from jedi._compatibility import force_unicode, is_py3 from jedi.inference import compiled from jedi.inference import analysis -from jedi.inference import recursion from jedi.inference.lazy_value import LazyKnownValue, LazyKnownValues, \ LazyTreeValue from jedi.inference.helpers import get_int_or_none, is_string, \ - infer_call_of_leaf, reraise_getitem_errors, SimpleGetItemNotFound + reraise_getitem_errors, SimpleGetItemNotFound from jedi.inference.utils import safe_property, to_list from jedi.inference.cache import inference_state_method_cache from jedi.inference.filters import LazyAttributeOverwrite, publish_method from jedi.inference.base_value import ValueSet, Value, NO_VALUES, \ - ContextualizedNode, iterate_values, HelperValueMixin, sentinel, \ + ContextualizedNode, iterate_values, sentinel, \ LazyValueWrapper from jedi.parser_utils import get_sync_comp_fors from jedi.inference.context import CompForContext +from jedi.inference.value.dynamic_arrays import check_array_additions class IterableMixin(object): @@ -385,7 +365,7 @@ class SequenceLiteralValue(Sequence): yield LazyKnownValue(Slice(self._defining_context, None, None, None)) else: yield LazyTreeValue(self._defining_context, node) - for addition in _check_array_additions(self._defining_context, self): + for addition in check_array_additions(self._defining_context, self): yield addition def py__len__(self): @@ -632,143 +612,6 @@ def unpack_tuple_to_dict(value, types, exprlist): raise NotImplementedError -def _check_array_additions(context, sequence): - """ Just a mapper function for the internal _internal_check_array_additions """ - if sequence.array_type not in ('list', 'set'): - # TODO also check for dict updates - return NO_VALUES - - return _internal_check_array_additions(context, sequence) - - -@inference_state_method_cache(default=NO_VALUES) -@debug.increase_indent -def _internal_check_array_additions(context, sequence): - """ - Checks if a `Array` has "add" (append, insert, extend) statements: - - >>> a = [""] - >>> a.append(1) - """ - from jedi.inference import arguments - - debug.dbg('Dynamic array search for %s' % sequence, color='MAGENTA') - module_context = context.get_root_context() - if not settings.dynamic_array_additions or module_context.is_compiled(): - debug.dbg('Dynamic array search aborted.', color='MAGENTA') - return NO_VALUES - - def find_additions(context, arglist, add_name): - params = list(arguments.TreeArguments(context.inference_state, context, arglist).unpack()) - result = set() - if add_name in ['insert']: - params = params[1:] - if add_name in ['append', 'add', 'insert']: - for key, lazy_value in params: - result.add(lazy_value) - elif add_name in ['extend', 'update']: - for key, lazy_value in params: - result |= set(lazy_value.infer().iterate()) - return result - - temp_param_add, settings.dynamic_params_for_other_modules = \ - settings.dynamic_params_for_other_modules, False - - is_list = sequence.name.string_name == 'list' - search_names = (['append', 'extend', 'insert'] if is_list else ['add', 'update']) - - added_types = set() - for add_name in search_names: - try: - possible_names = module_context.tree_node.get_used_names()[add_name] - except KeyError: - continue - else: - for name in possible_names: - value_node = context.tree_node - if not (value_node.start_pos < name.start_pos < value_node.end_pos): - continue - trailer = name.parent - power = trailer.parent - trailer_pos = power.children.index(trailer) - try: - execution_trailer = power.children[trailer_pos + 1] - except IndexError: - continue - else: - if execution_trailer.type != 'trailer' \ - or execution_trailer.children[0] != '(' \ - or execution_trailer.children[1] == ')': - continue - - random_context = context.create_context(name) - - with recursion.execution_allowed(context.inference_state, power) as allowed: - if allowed: - found = infer_call_of_leaf( - random_context, - name, - cut_own_trailer=True - ) - if sequence in found: - # The arrays match. Now add the results - added_types |= find_additions( - random_context, - execution_trailer.children[1], - add_name - ) - - # reset settings - settings.dynamic_params_for_other_modules = temp_param_add - debug.dbg('Dynamic array result %s' % added_types, color='MAGENTA') - return added_types - - -def get_dynamic_array_instance(instance, arguments): - """Used for set() and list() instances.""" - ai = _ArrayInstance(instance, arguments) - from jedi.inference import arguments - return arguments.ValuesArguments([ValueSet([ai])]) - - -class _ArrayInstance(HelperValueMixin): - """ - Used for the usage of set() and list(). - This is definitely a hack, but a good one :-) - It makes it possible to use set/list conversions. - - This is not a proper context, because it doesn't have to be. It's not used - in the wild, it's just used within typeshed as an argument to `__init__` - for set/list and never used in any other place. - """ - def __init__(self, instance, var_args): - self.instance = instance - self.var_args = var_args - - def py__class__(self): - tuple_, = self.instance.inference_state.builtins_module.py__getattribute__('tuple') - return tuple_ - - def py__iter__(self, contextualized_node=None): - var_args = self.var_args - try: - _, lazy_value = next(var_args.unpack()) - except StopIteration: - pass - else: - for lazy in lazy_value.infer().iterate(): - yield lazy - - from jedi.inference import arguments - if isinstance(var_args, arguments.TreeArguments): - additions = _internal_check_array_additions(var_args.context, self.instance) - for addition in additions: - yield addition - - def iterate(self, contextualized_node=None, is_async=False): - return self.py__iter__(contextualized_node) - - class Slice(LazyValueWrapper): def __init__(self, python_context, start, stop, step): self.inference_state = python_context.inference_state