205 lines
7.5 KiB
Python
205 lines
7.5 KiB
Python
|
"""
|
||
|
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 whether 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, \
|
||
|
ValueWrapper
|
||
|
from jedi.inference.lazy_value import LazyKnownValues
|
||
|
from jedi.inference.helpers import infer_call_of_leaf
|
||
|
from jedi.inference.cache import inference_state_method_cache
|
||
|
|
||
|
_sentinel = object()
|
||
|
|
||
|
|
||
|
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 = _DynamicArrayAdditions(instance, arguments)
|
||
|
from jedi.inference import arguments
|
||
|
return arguments.ValuesArguments([ValueSet([ai])])
|
||
|
|
||
|
|
||
|
class _DynamicArrayAdditions(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, arguments):
|
||
|
self._instance = instance
|
||
|
self._arguments = arguments
|
||
|
|
||
|
def py__class__(self):
|
||
|
tuple_, = self._instance.inference_state.builtins_module.py__getattribute__('tuple')
|
||
|
return tuple_
|
||
|
|
||
|
def py__iter__(self, contextualized_node=None):
|
||
|
arguments = self._arguments
|
||
|
try:
|
||
|
_, lazy_value = next(arguments.unpack())
|
||
|
except StopIteration:
|
||
|
pass
|
||
|
else:
|
||
|
for lazy in lazy_value.infer().iterate():
|
||
|
yield lazy
|
||
|
|
||
|
from jedi.inference.arguments import TreeArguments
|
||
|
if isinstance(arguments, TreeArguments):
|
||
|
additions = _internal_check_array_additions(arguments.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 _Modification(ValueWrapper):
|
||
|
def __init__(self, wrapped_value, assigned_values, contextualized_key):
|
||
|
super(_Modification, self).__init__(wrapped_value)
|
||
|
self._assigned_values = assigned_values
|
||
|
self._contextualized_key = contextualized_key
|
||
|
|
||
|
def py__getitem__(self, *args, **kwargs):
|
||
|
return self._wrapped_value.py__getitem__(*args, **kwargs) | self._assigned_values
|
||
|
|
||
|
def py__simple_getitem__(self, index):
|
||
|
actual = [
|
||
|
v.get_safe_value(_sentinel)
|
||
|
for v in self._contextualized_key.infer()
|
||
|
]
|
||
|
if index in actual:
|
||
|
return self._assigned_values
|
||
|
return self._wrapped_value.py__simple_getitem__(index)
|
||
|
|
||
|
|
||
|
class DictModification(_Modification):
|
||
|
def py__iter__(self, contextualized_node=None):
|
||
|
for lazy_context in self._wrapped_value.py__iter__(contextualized_node):
|
||
|
yield lazy_context
|
||
|
yield self._contextualized_key
|
||
|
|
||
|
def get_key_values(self):
|
||
|
return self._wrapped_value.get_key_values() | self._contextualized_key.infer()
|
||
|
|
||
|
|
||
|
class ListModification(_Modification):
|
||
|
def py__iter__(self, contextualized_node=None):
|
||
|
for lazy_context in self._wrapped_value.py__iter__(contextualized_node):
|
||
|
yield lazy_context
|
||
|
yield LazyKnownValues(self._assigned_values)
|