198 lines
8.1 KiB
Python
198 lines
8.1 KiB
Python
|
"""
|
||
|
Type inference of Python code in |jedi| is based on three assumptions:
|
||
|
|
||
|
* The code uses as least side effects as possible. Jedi understands certain
|
||
|
list/tuple/set modifications, but there's no guarantee that Jedi detects
|
||
|
everything (list.append in different modules for example).
|
||
|
* No magic is being used:
|
||
|
|
||
|
- metaclasses
|
||
|
- ``setattr()`` / ``__import__()``
|
||
|
- writing to ``globals()``, ``locals()``, ``object.__dict__``
|
||
|
* The programmer is not a total dick, e.g. like `this
|
||
|
<https://github.com/davidhalter/jedi/issues/24>`_ :-)
|
||
|
|
||
|
The actual algorithm is based on a principle I call lazy type inference. That
|
||
|
said, the typical entry point for static analysis is calling
|
||
|
``infer_expr_stmt``. There's separate logic for autocompletion in the API, the
|
||
|
inference_state is all about inferring an expression.
|
||
|
|
||
|
TODO this paragraph is not what jedi does anymore, it's similar, but not the
|
||
|
same.
|
||
|
|
||
|
Now you need to understand what follows after ``infer_expr_stmt``. Let's
|
||
|
make an example::
|
||
|
|
||
|
import datetime
|
||
|
datetime.date.toda# <-- cursor here
|
||
|
|
||
|
First of all, this module doesn't care about completion. It really just cares
|
||
|
about ``datetime.date``. At the end of the procedure ``infer_expr_stmt`` will
|
||
|
return the ``date`` class.
|
||
|
|
||
|
To *visualize* this (simplified):
|
||
|
|
||
|
- ``InferenceState.infer_expr_stmt`` doesn't do much, because there's no assignment.
|
||
|
- ``Context.infer_node`` cares for resolving the dotted path
|
||
|
- ``InferenceState.find_types`` searches for global definitions of datetime, which
|
||
|
it finds in the definition of an import, by scanning the syntax tree.
|
||
|
- Using the import logic, the datetime module is found.
|
||
|
- Now ``find_types`` is called again by ``infer_node`` to find ``date``
|
||
|
inside the datetime module.
|
||
|
|
||
|
Now what would happen if we wanted ``datetime.date.foo.bar``? Two more
|
||
|
calls to ``find_types``. However the second call would be ignored, because the
|
||
|
first one would return nothing (there's no foo attribute in ``date``).
|
||
|
|
||
|
What if the import would contain another ``ExprStmt`` like this::
|
||
|
|
||
|
from foo import bar
|
||
|
Date = bar.baz
|
||
|
|
||
|
Well... You get it. Just another ``infer_expr_stmt`` recursion. It's really
|
||
|
easy. Python can obviously get way more complicated then this. To understand
|
||
|
tuple assignments, list comprehensions and everything else, a lot more code had
|
||
|
to be written.
|
||
|
|
||
|
Jedi has been tested very well, so you can just start modifying code. It's best
|
||
|
to write your own test first for your "new" feature. Don't be scared of
|
||
|
breaking stuff. As long as the tests pass, you're most likely to be fine.
|
||
|
|
||
|
I need to mention now that lazy type inference is really good because it
|
||
|
only *inferes* what needs to be *inferred*. All the statements and modules
|
||
|
that are not used are just being ignored.
|
||
|
"""
|
||
|
import parso
|
||
|
from jedi.file_io import FileIO
|
||
|
|
||
|
from jedi import debug
|
||
|
from jedi import settings
|
||
|
from jedi.inference import imports
|
||
|
from jedi.inference import recursion
|
||
|
from jedi.inference.cache import inference_state_function_cache
|
||
|
from jedi.inference import helpers
|
||
|
from jedi.inference.names import TreeNameDefinition
|
||
|
from jedi.inference.base_value import ContextualizedNode, \
|
||
|
ValueSet, iterate_values
|
||
|
from jedi.inference.value import ClassValue, FunctionValue
|
||
|
from jedi.inference.syntax_tree import infer_expr_stmt, \
|
||
|
check_tuple_assignments, tree_name_to_values
|
||
|
from jedi.inference.imports import follow_error_node_imports_if_possible
|
||
|
from jedi.plugins import plugin_manager
|
||
|
|
||
|
|
||
|
class InferenceState(object):
|
||
|
def __init__(self, project, environment=None, script_path=None):
|
||
|
if environment is None:
|
||
|
environment = project.get_environment()
|
||
|
self.environment = environment
|
||
|
self.script_path = script_path
|
||
|
self.compiled_subprocess = environment.get_inference_state_subprocess(self)
|
||
|
self.grammar = environment.get_grammar()
|
||
|
|
||
|
self.latest_grammar = parso.load_grammar(version='3.7')
|
||
|
self.memoize_cache = {} # for memoize decorators
|
||
|
self.module_cache = imports.ModuleCache() # does the job of `sys.modules`.
|
||
|
self.stub_module_cache = {} # Dict[Tuple[str, ...], Optional[ModuleValue]]
|
||
|
self.compiled_cache = {} # see `inference.compiled.create()`
|
||
|
self.inferred_element_counts = {}
|
||
|
self.mixed_cache = {} # see `inference.compiled.mixed._create()`
|
||
|
self.analysis = []
|
||
|
self.dynamic_params_depth = 0
|
||
|
self.is_analysis = False
|
||
|
self.project = project
|
||
|
self.access_cache = {}
|
||
|
self.allow_descriptor_getattr = False
|
||
|
self.flow_analysis_enabled = True
|
||
|
|
||
|
self.reset_recursion_limitations()
|
||
|
|
||
|
def import_module(self, import_names, sys_path=None, prefer_stubs=True):
|
||
|
return imports.import_module_by_names(
|
||
|
self, import_names, sys_path, prefer_stubs=prefer_stubs)
|
||
|
|
||
|
@staticmethod
|
||
|
@plugin_manager.decorate()
|
||
|
def execute(value, arguments):
|
||
|
debug.dbg('execute: %s %s', value, arguments)
|
||
|
with debug.increase_indent_cm():
|
||
|
value_set = value.py__call__(arguments=arguments)
|
||
|
debug.dbg('execute result: %s in %s', value_set, value)
|
||
|
return value_set
|
||
|
|
||
|
@property
|
||
|
@inference_state_function_cache()
|
||
|
def builtins_module(self):
|
||
|
module_name = u'builtins'
|
||
|
if self.environment.version_info.major == 2:
|
||
|
module_name = u'__builtin__'
|
||
|
builtins_module, = self.import_module((module_name,), sys_path=())
|
||
|
return builtins_module
|
||
|
|
||
|
@property
|
||
|
@inference_state_function_cache()
|
||
|
def typing_module(self):
|
||
|
typing_module, = self.import_module((u'typing',))
|
||
|
return typing_module
|
||
|
|
||
|
def reset_recursion_limitations(self):
|
||
|
self.recursion_detector = recursion.RecursionDetector()
|
||
|
self.execution_recursion_detector = recursion.ExecutionRecursionDetector(self)
|
||
|
|
||
|
def get_sys_path(self, **kwargs):
|
||
|
"""Convenience function"""
|
||
|
return self.project._get_sys_path(self, **kwargs)
|
||
|
|
||
|
def infer(self, context, name):
|
||
|
def_ = name.get_definition(import_name_always=True)
|
||
|
if def_ is not None:
|
||
|
type_ = def_.type
|
||
|
is_classdef = type_ == 'classdef'
|
||
|
if is_classdef or type_ == 'funcdef':
|
||
|
if is_classdef:
|
||
|
c = ClassValue(self, context, name.parent)
|
||
|
else:
|
||
|
c = FunctionValue.from_context(context, name.parent)
|
||
|
return ValueSet([c])
|
||
|
|
||
|
if type_ == 'expr_stmt':
|
||
|
is_simple_name = name.parent.type not in ('power', 'trailer')
|
||
|
if is_simple_name:
|
||
|
return infer_expr_stmt(context, def_, name)
|
||
|
if type_ == 'for_stmt':
|
||
|
container_types = context.infer_node(def_.children[3])
|
||
|
cn = ContextualizedNode(context, def_.children[3])
|
||
|
for_types = iterate_values(container_types, cn)
|
||
|
n = TreeNameDefinition(context, name)
|
||
|
return check_tuple_assignments(n, for_types)
|
||
|
if type_ in ('import_from', 'import_name'):
|
||
|
return imports.infer_import(context, name)
|
||
|
if type_ == 'with_stmt':
|
||
|
return tree_name_to_values(self, context, name)
|
||
|
elif type_ == 'param':
|
||
|
return context.py__getattribute__(name.value, position=name.end_pos)
|
||
|
else:
|
||
|
result = follow_error_node_imports_if_possible(context, name)
|
||
|
if result is not None:
|
||
|
return result
|
||
|
|
||
|
return helpers.infer_call_of_leaf(context, name)
|
||
|
|
||
|
def parse_and_get_code(self, code=None, path=None, encoding='utf-8',
|
||
|
use_latest_grammar=False, file_io=None, **kwargs):
|
||
|
if code is None:
|
||
|
if file_io is None:
|
||
|
file_io = FileIO(path)
|
||
|
code = file_io.read()
|
||
|
# We cannot just use parso, because it doesn't use errors='replace'.
|
||
|
code = parso.python_bytes_to_unicode(code, encoding=encoding, errors='replace')
|
||
|
|
||
|
if len(code) > settings._cropped_file_size:
|
||
|
code = code[:settings._cropped_file_size]
|
||
|
|
||
|
grammar = self.latest_grammar if use_latest_grammar else self.grammar
|
||
|
return grammar.parse(code=code, path=path, file_io=file_io, **kwargs), code
|
||
|
|
||
|
def parse(self, *args, **kwargs):
|
||
|
return self.parse_and_get_code(*args, **kwargs)[0]
|