forked from VimPlug/jedi
Instead of having both next and execution as attributes, we now only have next, because it's an execution if there's an array.
237 lines
7.7 KiB
Python
237 lines
7.7 KiB
Python
"""
|
|
Module for statical analysis.
|
|
"""
|
|
|
|
from jedi import debug
|
|
from jedi.parser import representation as pr
|
|
from jedi.evaluate.compiled import CompiledObject
|
|
|
|
|
|
CODES = {
|
|
'attribute-error': (1, AttributeError, 'Potential AttributeError.'),
|
|
'name-error': (2, NameError, 'Potential NameError.'),
|
|
'import-error': (3, ImportError, 'Potential ImportError.'),
|
|
'type-error-generator': (4, TypeError, "TypeError: 'generator' object is not subscriptable."),
|
|
'type-error-too-many-arguments': (5, TypeError, None),
|
|
'type-error-too-few-arguments': (6, TypeError, None),
|
|
'type-error-keyword-argument': (7, TypeError, None),
|
|
'type-error-multiple-values': (8, TypeError, None),
|
|
'type-error-star-star': (9, TypeError, None),
|
|
'type-error-star': (10, TypeError, None),
|
|
'type-error-operation': (11, TypeError, None),
|
|
}
|
|
|
|
|
|
class Error(object):
|
|
def __init__(self, name, module_path, start_pos, message=None):
|
|
self.path = module_path
|
|
self._start_pos = start_pos
|
|
self.name = name
|
|
if message is None:
|
|
message = CODES[self.name][2]
|
|
self.message = message
|
|
|
|
@property
|
|
def line(self):
|
|
return self._start_pos[0]
|
|
|
|
@property
|
|
def column(self):
|
|
return self._start_pos[1]
|
|
|
|
@property
|
|
def code(self):
|
|
# The class name start
|
|
first = self.__class__.__name__[0]
|
|
return first + str(CODES[self.name][0])
|
|
|
|
def __unicode__(self):
|
|
return '%s:%s:%s: %s %s' % (self.path, self.line, self.column,
|
|
self.code, self.message)
|
|
|
|
def __str__(self):
|
|
return self.__unicode__()
|
|
|
|
def __eq__(self, other):
|
|
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)
|
|
|
|
def __hash__(self):
|
|
return hash((self.path, self._start_pos, self.name))
|
|
|
|
def __repr__(self):
|
|
return '<%s %s: %s@%s,%s>' % (self.__class__.__name__,
|
|
self.name, self.path,
|
|
self._start_pos[0], self._start_pos[1])
|
|
|
|
|
|
class Warning(Error):
|
|
pass
|
|
|
|
|
|
def add(evaluator, name, jedi_obj, message=None, typ=Error, payload=None):
|
|
exception = CODES[name][1]
|
|
if _check_for_exception_catch(evaluator, jedi_obj, exception, payload):
|
|
return
|
|
|
|
module_path = jedi_obj.get_parent_until().path
|
|
instance = typ(name, module_path, jedi_obj.start_pos, message)
|
|
debug.warning(str(instance))
|
|
evaluator.analysis.append(instance)
|
|
|
|
|
|
def _check_for_setattr(instance):
|
|
"""
|
|
Check if there's any setattr method inside an instance. If so, return True.
|
|
"""
|
|
module = instance.get_parent_until()
|
|
try:
|
|
stmts = module.used_names['setattr']
|
|
except KeyError:
|
|
return False
|
|
|
|
return any(instance.start_pos < stmt.start_pos < instance.end_pos
|
|
for stmt in stmts)
|
|
|
|
|
|
def add_attribute_error(evaluator, scope, name_part):
|
|
message = ('AttributeError: %s has no attribute %s.' % (scope, name_part))
|
|
from jedi.evaluate.representation import Instance
|
|
# Check for __getattr__/__getattribute__ existance and issue a warning
|
|
# instead of an error, if that happens.
|
|
if isinstance(scope, Instance):
|
|
typ = Warning
|
|
try:
|
|
scope.get_subscope_by_name('__getattr__')
|
|
except KeyError:
|
|
try:
|
|
scope.get_subscope_by_name('__getattribute__')
|
|
except KeyError:
|
|
if not _check_for_setattr(scope):
|
|
typ = Error
|
|
else:
|
|
typ = Error
|
|
|
|
payload = scope, name_part
|
|
add(evaluator, 'attribute-error', name_part, message, typ, payload)
|
|
|
|
|
|
def _check_for_exception_catch(evaluator, jedi_obj, 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`).
|
|
Also checks `hasattr` for AttributeErrors and uses the `payload` to compare
|
|
it.
|
|
Returns True if the exception was catched.
|
|
"""
|
|
def check_match(cls):
|
|
try:
|
|
return isinstance(cls, CompiledObject) and issubclass(exception, cls.obj)
|
|
except TypeError:
|
|
return False
|
|
|
|
def check_try_for_except(obj):
|
|
while obj.next is not None:
|
|
obj = obj.next
|
|
if not obj.inputs:
|
|
# No import implies a `except:` catch, which catches
|
|
# everything.
|
|
return True
|
|
|
|
for i in obj.inputs:
|
|
except_classes = evaluator.eval_statement(i)
|
|
for cls in except_classes:
|
|
from jedi.evaluate import iterable
|
|
if isinstance(cls, iterable.Array) and cls.type == 'tuple':
|
|
# multiple exceptions
|
|
for c in cls.values():
|
|
if check_match(c):
|
|
return True
|
|
else:
|
|
if check_match(cls):
|
|
return True
|
|
return False
|
|
|
|
def check_hasattr(stmt):
|
|
expression_list = stmt.expression_list()
|
|
try:
|
|
assert len(expression_list) == 1
|
|
call = expression_list[0]
|
|
assert isinstance(call, pr.Call) and str(call.name) == 'hasattr'
|
|
assert call.next_is_execution()
|
|
execution = call.next
|
|
assert execution and len(execution) == 2
|
|
|
|
# check if the names match
|
|
names = evaluator.eval_statement(execution[1])
|
|
assert len(names) == 1 and isinstance(names[0], CompiledObject)
|
|
assert names[0].obj == str(payload[1])
|
|
|
|
objects = evaluator.eval_statement(execution[0])
|
|
return payload[0] in objects
|
|
except AssertionError:
|
|
pass
|
|
return False
|
|
|
|
obj = jedi_obj
|
|
while obj is not None and not obj.isinstance(pr.Function, pr.Class):
|
|
if obj.isinstance(pr.Flow):
|
|
# try/except catch check
|
|
if obj.command == 'try' and check_try_for_except(obj):
|
|
return True
|
|
# hasattr check
|
|
if exception == AttributeError and obj.command in ('if', 'while'):
|
|
if obj.inputs and check_hasattr(obj.inputs[0]):
|
|
return True
|
|
obj = obj.parent
|
|
|
|
return False
|
|
|
|
|
|
def get_module_statements(module):
|
|
"""
|
|
Returns the statements used in a module. All these statements should be
|
|
evaluated to check for potential exceptions.
|
|
"""
|
|
def add_stmts(stmts):
|
|
new = set()
|
|
for stmt in stmts:
|
|
if isinstance(stmt, pr.Flow):
|
|
while stmt is not None:
|
|
new |= add_stmts(stmt.inputs)
|
|
stmt = stmt.next
|
|
continue
|
|
if isinstance(stmt, pr.KeywordStatement):
|
|
stmt = stmt.stmt
|
|
if stmt is None:
|
|
continue
|
|
|
|
for expression in stmt.expression_list():
|
|
if isinstance(expression, pr.Array):
|
|
new |= add_stmts(expression.values)
|
|
|
|
if isinstance(expression, pr.StatementElement):
|
|
for element in expression.generate_call_path():
|
|
if isinstance(element, pr.Array):
|
|
new |= add_stmts(element.values)
|
|
new.add(stmt)
|
|
return new
|
|
|
|
stmts = set()
|
|
imports = set()
|
|
for scope in module.walk():
|
|
imports |= set(scope.imports)
|
|
stmts |= add_stmts(scope.statements)
|
|
stmts |= add_stmts(r for r in scope.returns if r is not None)
|
|
|
|
try:
|
|
decorators = scope.decorators
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
stmts |= add_stmts(decorators)
|
|
return stmts, imports
|