1
0
forked from VimPlug/jedi

Some analysis improvements.

This commit is contained in:
Dave Halter
2016-12-11 15:03:19 +01:00
parent 2be5da3f85
commit eaf0100446
12 changed files with 137 additions and 131 deletions

View File

@@ -13,7 +13,6 @@ import os
import warnings
import sys
from jedi._compatibility import unicode
from jedi.parser import load_grammar
from jedi.parser import tree
from jedi.parser.fast import FastParser
@@ -195,7 +194,7 @@ class Script(object):
if leaf is None:
return []
context = self._evaluator.create_context(self._get_module(), leaf.parent)
context = self._evaluator.create_context(self._get_module(), leaf)
definitions = helpers.evaluate_goto_definition(self._evaluator, context, leaf)
names = [s.name for s in definitions]
@@ -328,11 +327,12 @@ class Script(object):
self._evaluator.analysis_modules = [module_node]
try:
for node in module_node.nodes_to_execute():
context = self._get_module().create_context(node)
if node.type in ('funcdef', 'classdef'):
if node.type == 'classdef':
continue
raise NotImplementedError
er.Function(self._evaluator, node).get_decorated_func()
# TODO This is stupid, should be private
from jedi.evaluate.finder import _name_to_types
# Resolve the decorators.
_name_to_types(self._evaluator, context, node.children[1])
elif isinstance(node, tree.Import):
import_names = set(node.get_defined_names())
if node.is_nested():
@@ -340,12 +340,12 @@ class Script(object):
for n in import_names:
imports.ImportWrapper(context, n).follow()
elif node.type == 'expr_stmt':
types = self._evaluator.eval_element(node)
types = context.eval_node(node)
for testlist in node.children[:-1:2]:
# Iterate tuples.
unpack_tuple_to_dict(self._evaluator, types, testlist)
else:
try_iter_content(self._evaluator.goto_definitions(node))
try_iter_content(self._evaluator.goto_definitions(context, node))
self._evaluator.reset_recursion_limitations()
ana = [a for a in self._evaluator.analysis if self.path == a.path]

View File

@@ -133,7 +133,7 @@ class BaseDefinition(object):
>>> defs = sorted(defs, key=lambda d: d.line)
>>> defs # doctest: +NORMALIZE_WHITESPACE
[<Definition module keyword>, <Definition class C>,
<Definition class D>, <Definition def f>]
<Definition instance D>, <Definition def f>]
Finally, here is what you can get from :attr:`type`:
@@ -489,7 +489,7 @@ class Completion(BaseDefinition):
return '%s: %s%s' % (t, desc, line)
def __repr__(self):
return '<%s: %s>' % (type(self).__name__, self._name)
return '<%s: %s>' % (type(self).__name__, self._name.string_name)
@memoize_method
def _follow_statements_imports(self):
@@ -556,7 +556,7 @@ class Definition(BaseDefinition):
"""
typ = self.type
tree_name = self._name.tree_name
if typ in ('function', 'class', 'module') or tree_name is None:
if typ in ('function', 'class', 'module', 'instance') or tree_name is None:
if typ == 'function':
# For the description we want a short and a pythonic way.
typ = 'def'
@@ -743,8 +743,8 @@ class CallSignature(Definition):
return self._executable.get_parent_until()
def __repr__(self):
return '<%s: %s index %s>' % (type(self).__name__, self._name,
self.index)
return '<%s: %s index %s>' % \
(type(self).__name__, self._name.string_name, self.index)
class _Param(Definition):

View File

@@ -9,19 +9,31 @@ def usages(evaluator, definition_names, mods):
"""
:param definitions: list of Name
"""
def resolve_names(definition_names):
for name in definition_names:
if name.api_type == 'module':
found = False
for context in name.infer():
found = True
yield context.name
if not found:
yield name
else:
yield name
def compare_array(definition_names):
""" `definitions` are being compared by module/start_pos, because
sometimes the id's of the objects change (e.g. executions).
"""
return [
(d.get_root_context(), d.start_pos)
for d in definition_names
(name.get_root_context(), name.start_pos)
for name in resolve_names(definition_names)
]
search_name = list(definition_names)[0].string_name
compare_definitions = compare_array(definition_names)
mods = mods | set([d.get_root_context() for d in definition_names])
definition_names = set(definition_names)
definition_names = set(resolve_names(definition_names))
for m in imports.get_modules_containing_name(evaluator, mods, search_name):
if isinstance(m, ModuleContext):
for name_node in m.module_node.used_names.get(search_name, []):

View File

@@ -169,10 +169,10 @@ class Evaluator(object):
dct = {str(for_stmt.children[1]): lazy_context.infer()}
with helpers.predefine_names(context, for_stmt, dct):
t = self.eval_element(context, rhs)
left = precedence.calculate(self, left, operator, t)
left = precedence.calculate(self, context, left, operator, t)
types = left
else:
types = precedence.calculate(self, left, operator, types)
types = precedence.calculate(self, context, left, operator, types)
debug.dbg('eval_statement result %s', types)
return types
@@ -180,9 +180,16 @@ class Evaluator(object):
if isinstance(context, iterable.CompForContext):
return self._eval_element_not_cached(context, element)
if_stmt = element.get_parent_until((tree.IfStmt, tree.ForStmt, tree.IsScope))
if_stmt = element
while if_stmt is not None:
if_stmt = if_stmt.parent
if if_stmt.type in ('if_stmt', 'for_stmt'):
break
if if_stmt.is_scope():
if_stmt = None
break
predefined_if_name_dict = context.predefined_names.get(if_stmt)
if predefined_if_name_dict is None and isinstance(if_stmt, tree.IfStmt):
if predefined_if_name_dict is None and if_stmt and if_stmt.type == 'if_stmt':
if_stmt_test = if_stmt.children[1]
name_dicts = [{}]
# If we already did a check, we don't want to do it again -> If
@@ -274,7 +281,7 @@ class Evaluator(object):
for trailer in element.children[1:]:
if trailer == '**': # has a power operation.
right = self.eval_element(context, element.children[2])
types = set(precedence.calculate(self, types, trailer, right))
types = set(precedence.calculate(self, context, types, trailer, right))
break
types = self.eval_trailer(context, types, trailer)
elif element.type in ('testlist_star_expr', 'testlist',):
@@ -344,7 +351,7 @@ class Evaluator(object):
types = self._eval_atom(context, c[0])
for string in c[1:]:
right = self._eval_atom(context, string)
types = precedence.calculate(self, types, '+', right)
types = precedence.calculate(self, context, types, '+', right)
return types
# Parentheses without commas are not tuples.
elif c[0] == '(' and not len(c) == 2 \
@@ -491,11 +498,6 @@ class Evaluator(object):
# a name it's something you can "goto" again.
return [TreeNameDefinition(context, name)]
elif isinstance(par, (tree.Param, tree.Function, tree.Class)) and par.name is name:
if par.type in ('funcdef', 'classdef', 'module'):
if par.type == 'funcdef':
return [context.function_context.name]
else:
return [context.name]
return [TreeNameDefinition(context, name)]
elif isinstance(stmt, tree.Import):
module_names = imports.ImportWrapper(context, name).follow(is_goto=True)
@@ -600,5 +602,9 @@ class Evaluator(object):
if node_is_context and node.is_scope():
scope_node = node
else:
if node.parent.type in ('funcdef', 'classdef'):
# When we're on class/function names/leafs that define the
# object itself and not its contents.
node = node.parent
scope_node = parent_scope(node)
return from_scope_node(scope_node)

View File

@@ -58,8 +58,8 @@ class Error(object):
return self.__unicode__()
def __eq__(self, other):
return (self.path == other.path and self.name == other.name
and self._start_pos == other._start_pos)
return (self.path == other.path and self.name == other.name and
self._start_pos == other._start_pos)
def __ne__(self, other):
return not self.__eq__(other)
@@ -77,23 +77,19 @@ class Warning(Error):
pass
def add(evaluator, name, jedi_obj, message=None, typ=Error, payload=None):
def add(context, name, jedi_name, message=None, typ=Error, payload=None):
return
from jedi.evaluate.iterable import MergedNodes
while isinstance(jedi_obj, MergedNodes):
if len(jedi_obj) != 1:
# TODO is this kosher?
return
jedi_obj = list(jedi_obj)[0]
from jedi.evaluate import Evaluator
if isinstance(context, Evaluator):
raise 1
exception = CODES[name][1]
if _check_for_exception_catch(evaluator, jedi_obj, exception, payload):
if _check_for_exception_catch(context, jedi_name, exception, payload):
return
module_path = jedi_obj.get_parent_until().path
instance = typ(name, module_path, jedi_obj.start_pos, message)
module_path = jedi_name.get_root_node().path
instance = typ(name, module_path, jedi_name.start_pos, message)
debug.warning(str(instance), format=False)
evaluator.analysis.append(instance)
context.evaluator.analysis.append(instance)
def _check_for_setattr(instance):
@@ -114,25 +110,29 @@ def _check_for_setattr(instance):
for stmt in stmts)
def add_attribute_error(evaluator, scope, name):
message = ('AttributeError: %s has no attribute %s.' % (scope, name))
from jedi.evaluate.instance import AbstractInstanceContext
def add_attribute_error(context, name):
message = ('AttributeError: %s has no attribute %s.' % (context, name))
from jedi.evaluate.instance import AbstractInstanceContext, CompiledInstanceName
# Check for __getattr__/__getattribute__ existance and issue a warning
# instead of an error, if that happens.
if isinstance(scope, AbstractInstanceContext):
typ = Error
if isinstance(context, AbstractInstanceContext):
slot_names = context.get_function_slot_names('__getattr__') + \
context.get_function_slot_names('__getattribute__')
for n in slot_names:
if isinstance(name, CompiledInstanceName) and \
n.parent_context.obj == object:
typ = Warning
if not (scope.get_function_slot_names('__getattr__') or
scope.get_function_slot_names('__getattribute__')):
if not _check_for_setattr(scope):
typ = Error
else:
typ = Error
break
payload = scope, name
add(evaluator, 'attribute-error', name, message, typ, payload)
if _check_for_setattr(context):
typ = Warning
payload = context, name
add(context, 'attribute-error', name, message, typ, payload)
def _check_for_exception_catch(evaluator, jedi_obj, exception, payload=None):
def _check_for_exception_catch(context, jedi_name, exception, payload=None):
"""
Checks if a jedi object (e.g. `Statement`) sits inside a try/catch and
doesn't count as an error (if equal to `exception`).
@@ -153,17 +153,18 @@ def _check_for_exception_catch(evaluator, jedi_obj, exception, payload=None):
colon = next(iterator)
suite = next(iterator)
if branch_type == 'try' \
and not (branch_type.start_pos < jedi_obj.start_pos <= suite.end_pos):
and not (branch_type.start_pos < jedi_name.start_pos <= suite.end_pos):
return False
for node in obj.except_clauses():
if node is None:
return True # An exception block that catches everything.
else:
except_classes = evaluator.eval_element(node)
except_classes = context.eval_node(node)
for cls in except_classes:
from jedi.evaluate import iterable
if isinstance(cls, iterable.Array) and cls.type == 'tuple':
if isinstance(cls, iterable.AbstractSequence) and \
cls.array_type == 'tuple':
# multiple exceptions
for typ in unite(cls.py__iter__()):
if check_match(typ, exception):
@@ -174,7 +175,7 @@ def _check_for_exception_catch(evaluator, jedi_obj, exception, payload=None):
def check_hasattr(node, suite):
try:
assert suite.start_pos <= jedi_obj.start_pos < suite.end_pos
assert suite.start_pos <= jedi_name.start_pos < suite.end_pos
assert node.type in ('power', 'atom_expr')
base = node.children[0]
assert base.type == 'name' and base.value == 'hasattr'
@@ -183,28 +184,28 @@ def _check_for_exception_catch(evaluator, jedi_obj, exception, payload=None):
arglist = trailer.children[1]
assert arglist.type == 'arglist'
from jedi.evaluate.param import Arguments
args = list(Arguments(evaluator, arglist).unpack())
args = list(Arguments(context, arglist).unpack())
# Arguments should be very simple
assert len(args) == 2
# Check name
key, values = args[1]
assert len(values) == 1
names = list(evaluator.eval_element(values[0]))
names = list(context.eval_node(values[0]))
assert len(names) == 1 and isinstance(names[0], CompiledObject)
assert names[0].obj == str(payload[1])
# Check objects
key, values = args[0]
assert len(values) == 1
objects = evaluator.eval_element(values[0])
objects = context.eval_node(values[0])
return payload[0] in objects
except AssertionError:
return False
obj = jedi_obj
while obj is not None and not obj.isinstance(tree.Function, tree.Class):
if obj.isinstance(tree.Flow):
obj = jedi_name
while obj is not None and not isinstance(obj, (tree.Function, tree.Class)):
if isinstance(obj, tree.Flow):
# try/except catch check
if obj.isinstance(tree.TryStmt) and check_try_for_except(obj, exception):
return True

View File

@@ -14,9 +14,7 @@ would check whether a flow has the form of ``if isinstance(a, type_or_tuple)``.
Unfortunately every other thing is being ignored (e.g. a == '' would be easy to
check for -> a is a string). There's big potential in these checks.
"""
from itertools import chain
from jedi._compatibility import unicode
from jedi.parser import tree
from jedi import debug
from jedi.common import unite
@@ -33,9 +31,7 @@ from jedi.evaluate import analysis
from jedi.evaluate import flow_analysis
from jedi.evaluate import param
from jedi.evaluate import helpers
from jedi.evaluate.context import TreeContext
from jedi.evaluate.cache import memoize_default
from jedi.evaluate.filters import get_global_filters, ContextName
from jedi.evaluate.filters import get_global_filters, TreeNameDefinition
def filter_after_position(names, position, origin=None):
@@ -128,22 +124,22 @@ class NameFinder(object):
isinstance(self._name.parent.parent, tree.Param)):
if isinstance(self._name, tree.Name):
if attribute_lookup:
analysis.add_attribute_error(self._evaluator,
self._context, self._name)
analysis.add_attribute_error(self._context, self._name)
else:
message = ("NameError: name '%s' is not defined."
% self._string_name)
analysis.add(self._evaluator, 'name-error', self._name,
message)
analysis.add(self._context, 'name-error', self._name, message)
return types
def get_filters(self, search_global=False):
def _get_origin_scope(self):
if isinstance(self._name, tree.Name):
origin_scope = self._name.get_parent_until(tree.Scope, reverse=True)
return self._name.get_parent_until(tree.Scope, reverse=True)
else:
origin_scope = None
return None
def get_filters(self, search_global=False):
origin_scope = self._get_origin_scope()
if search_global:
return get_global_filters(self._evaluator, self._context, self._position, origin_scope)
else:
@@ -321,15 +317,17 @@ class NameFinder(object):
# Add isinstance and other if/assert knowledge.
if not types and isinstance(self._name, tree.Name) and \
not isinstance(self._name_context, AbstractInstanceContext):
# Ignore FunctionExecution parents for now.
flow_scope = self._name
base_node = self._name_context.get_node()
if base_node.type == 'comp_for':
return types
while True:
flow_scope = flow_scope.get_parent_scope(include_flows=True)
n = _check_flow_information(self._name_context, flow_scope,
self._name, self._position)
if n is not None:
return n
if flow_scope == self._name_context.get_node():
if flow_scope == base_node:
break
return types

View File

@@ -1,6 +1,3 @@
from jedi.parser import tree
class Status(object):
lookup_table = {}

View File

@@ -147,10 +147,10 @@ class NestedImportModule(tree.Module):
self._nested_import)
def _add_error(evaluator, name, message=None):
if hasattr(name, 'parent'):
def _add_error(context, name, message=None):
# Should be a name, not a string!
analysis.add(evaluator, 'import-error', name, message)
if hasattr(name, 'parent'):
analysis.add(context, 'import-error', name, message)
def get_init_path(directory_path):
@@ -168,25 +168,21 @@ def get_init_path(directory_path):
class ImportName(AbstractNameDefinition):
start_pos = (1, 0)
def __init__(self, parent_module, string_name):
self.parent_module = parent_module
def __init__(self, parent_context, string_name):
self.parent_context = parent_context
self.string_name = string_name
def infer(self):
return Importer(
self.parent_module.evaluator,
self.parent_context.evaluator,
[self.string_name],
self.parent_module,
self.parent_context,
).follow()
def get_root_context(self):
# Not sure if this is correct.
return self.parent_context.get_root_context()
@property
def parent_context(self):
return self.parent_module
@property
def api_type(self):
return 'module'
@@ -195,18 +191,12 @@ class ImportName(AbstractNameDefinition):
class SubModuleName(ImportName):
def infer(self):
return Importer(
self.parent_module.evaluator,
self.parent_context.evaluator,
[self.string_name],
self.parent_module,
self.parent_context,
level=1
).follow()
@property
def parent_context(self):
# This is a bit of a special case. But it seems like it's working well.
# Since a SubModuleName is basically a lazy name to a module
return next(iter(self.infer()))
class Importer(object):
def __init__(self, evaluator, import_path, module_context, level=0):
@@ -249,7 +239,7 @@ class Importer(object):
if dir_name:
import_path.insert(0, dir_name)
else:
_add_error(self._evaluator, import_path[-1])
_add_error(module_context, import_path[-1])
import_path = []
# TODO add import error.
debug.warning('Attempted relative import beyond top-level package.')
@@ -336,7 +326,7 @@ class Importer(object):
method = parent_module.py__path__
except AttributeError:
# The module is not a package.
_add_error(self._evaluator, import_path[-1])
_add_error(parent_module, import_path[-1])
return set()
else:
paths = method()
@@ -351,7 +341,7 @@ class Importer(object):
except ImportError:
module_path = None
if module_path is None:
_add_error(self._evaluator, import_path[-1])
_add_error(parent_module, import_path[-1])
return set()
else:
parent_module = None
@@ -367,7 +357,7 @@ class Importer(object):
sys.path = temp
except ImportError:
# The module is not a package.
_add_error(self._evaluator, import_path[-1])
_add_error(parent_module, import_path[-1])
return set()
source = None

View File

@@ -63,10 +63,8 @@ class AbstractArguments():
Evaluates all arguments as a support for static analysis
(normally Jedi).
"""
raise DeprecationWarning
for key, element_values in self.unpack():
for element in element_values:
types = self._evaluator.eval_element(self.context, element)
for key, lazy_context in self.unpack():
types = lazy_context.infer()
try_iter_content(types)
@@ -260,7 +258,7 @@ def get_params(evaluator, parent_context, func, var_args):
% (func.name, key))
calling_va = _get_calling_var_args(evaluator, var_args)
if calling_va is not None:
analysis.add(evaluator, 'type-error-multiple-values',
analysis.add(parent_context, 'type-error-multiple-values',
calling_va, message=m)
else:
keys_used[key] = ExecutedParam(parent_context, key_param, var_args, argument)
@@ -302,7 +300,7 @@ def get_params(evaluator, parent_context, func, var_args):
calling_va = var_args.get_calling_var_args()
if calling_va is not None:
m = _error_argument_count(func, len(unpacked_va))
analysis.add(evaluator, 'type-error-too-few-arguments',
analysis.add(parent_context, 'type-error-too-few-arguments',
calling_va, message=m)
else:
result_arg = argument
@@ -323,13 +321,13 @@ def get_params(evaluator, parent_context, func, var_args):
calling_va = _get_calling_var_args(evaluator, var_args)
if calling_va is not None:
m = _error_argument_count(func, len(unpacked_va))
analysis.add(evaluator, 'type-error-too-few-arguments',
analysis.add(parent_context, 'type-error-too-few-arguments',
calling_va, message=m)
for key, argument in non_matching_keys.items():
m = "TypeError: %s() got an unexpected keyword argument '%s'." \
% (func.name, key)
analysis.add(evaluator, 'type-error-keyword-argument', argument.whatever, message=m)
analysis.add(parent_context, 'type-error-keyword-argument', argument.whatever, message=m)
remaining_arguments = list(var_arg_iterator)
if remaining_arguments:
@@ -354,7 +352,7 @@ def get_params(evaluator, parent_context, func, var_args):
# print('\t\tnonkw', non_kw_param.parent.var_args.argument_node, )
if origin_args not in [f.parent.parent for f in first_values]:
continue
analysis.add(evaluator, 'type-error-too-many-arguments',
analysis.add(parent_context, 'type-error-too-many-arguments',
v, message=m)
return result_params

View File

@@ -59,13 +59,13 @@ def calculate_children(evaluator, context, children):
types = context.eval_node(right)
# Otherwise continue, because of uncertainty.
else:
types = calculate(evaluator, types, operator,
types = calculate(evaluator, context, types, operator,
context.eval_node(right))
debug.dbg('calculate_children types %s', types)
return types
def calculate(evaluator, left_result, operator, right_result):
def calculate(evaluator, context, left_result, operator, right_result):
result = set()
if not left_result or not right_result:
# illegal slices e.g. cause left/right_result to be None
@@ -80,7 +80,7 @@ def calculate(evaluator, left_result, operator, right_result):
else:
for left in left_result:
for right in right_result:
result |= _element_calculate(evaluator, left, operator, right)
result |= _element_calculate(evaluator, context, left, operator, right)
return result
@@ -125,7 +125,7 @@ def _is_list(obj):
return isinstance(obj, iterable.AbstractSequence) and obj.array_type == 'list'
def _element_calculate(evaluator, left, operator, right):
def _element_calculate(evaluator, context, left, operator, right):
from jedi.evaluate import iterable, instance
l_is_num = _is_number(left)
r_is_num = _is_number(right)
@@ -173,7 +173,7 @@ def _element_calculate(evaluator, left, operator, right):
if operator in ('+', '-') and l_is_num != r_is_num \
and not (check(left) or check(right)):
message = "TypeError: unsupported operand type(s) for +: %s and %s"
analysis.add(evaluator, 'type-error-operation', operator,
analysis.add(context, 'type-error-operation', operator,
message % (left, right))
return set([left, right])

View File

@@ -58,11 +58,11 @@ def execute(evaluator, obj, arguments):
def _follow_param(evaluator, arguments, index):
try:
key, values = list(arguments.unpack())[index]
key, lazy_context = list(arguments.unpack())[index]
except IndexError:
return set()
else:
return unite(evaluator.eval_element(v) for v in values)
return lazy_context.infer()
def argument_clinic(string, want_obj=False, want_context=False, want_arguments=False):
@@ -219,7 +219,7 @@ def builtins_isinstance(evaluator, objects, types, arguments):
message = 'TypeError: isinstance() arg 2 must be a ' \
'class, type, or tuple of classes and types, ' \
'not %s.' % cls_or_tup
analysis.add(evaluator, 'type-error-isinstance', node, message)
analysis.add(cls_or_tup, 'type-error-isinstance', node, message)
return set(compiled.create(evaluator, x) for x in bool_results)
@@ -244,11 +244,12 @@ def collections_namedtuple(evaluator, obj, arguments):
_fields = list(_follow_param(evaluator, arguments, 1))[0]
if isinstance(_fields, compiled.CompiledObject):
fields = _fields.obj.replace(',', ' ').split()
elif isinstance(_fields, iterable.Array):
try:
fields = [v.obj for v in unite(_fields.py__iter__())]
except AttributeError:
return set()
elif isinstance(_fields, iterable.AbstractSequence):
fields = [
v.obj
for lazy_context in _fields.py__iter__()
for v in lazy_context.infer() if hasattr(v, 'obj')
]
else:
return set()
@@ -265,7 +266,7 @@ def collections_namedtuple(evaluator, obj, arguments):
# Parse source
generated_class = ParserWithRecovery(evaluator.grammar, unicode(source)).module.subscopes[0]
return set([er.Class(evaluator, generated_class)])
return set([er.ClassContext(evaluator, generated_class, evaluator.BUILTINS)])
@argument_clinic('first, /')

View File

@@ -71,9 +71,12 @@ def pytest_generate_tests(metafunc):
if 'static_analysis_case' in metafunc.fixturenames:
base_dir = os.path.join(os.path.dirname(__file__), 'static_analysis')
cases = list(collect_static_analysis_tests(base_dir, test_files))
metafunc.parametrize(
'static_analysis_case',
collect_static_analysis_tests(base_dir, test_files))
cases,
ids=[c.name for c in cases]
)
def collect_static_analysis_tests(base_dir, test_files):
@@ -91,6 +94,7 @@ class StaticAnalysisCase(object):
"""
def __init__(self, path):
self._path = path
self.name = os.path.basename(path)
with open(path) as f:
self._source = f.read()
@@ -98,7 +102,6 @@ class StaticAnalysisCase(object):
for line in self._source.splitlines():
self.skip = self.skip or run.skip_python_version(line)
def collect_comparison(self):
cases = []
for line_nr, line in enumerate(self._source.splitlines(), 1):