303 lines
11 KiB
Python
303 lines
11 KiB
Python
"""
|
|
Module for statical analysis.
|
|
"""
|
|
from jedi import debug
|
|
from jedi.parser import tree
|
|
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):
|
|
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]
|
|
|
|
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):
|
|
message = ('AttributeError: %s has no attribute %s.' % (scope, name))
|
|
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
|
|
add(evaluator, 'attribute-error', name, 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, exception):
|
|
try:
|
|
return isinstance(cls, CompiledObject) and issubclass(exception, cls.obj)
|
|
except TypeError:
|
|
return False
|
|
|
|
def check_try_for_except(obj, exception):
|
|
# Only nodes in try
|
|
iterator = iter(obj.children)
|
|
for branch_type in iterator:
|
|
colon = next(iterator)
|
|
suite = next(iterator)
|
|
if branch_type == 'try' \
|
|
and not (branch_type.start_pos < jedi_obj.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)
|
|
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, exception):
|
|
return True
|
|
else:
|
|
if check_match(cls, exception):
|
|
return True
|
|
|
|
def check_hasattr(node, suite):
|
|
try:
|
|
assert suite.start_pos <= jedi_obj.start_pos < suite.end_pos
|
|
assert node.type == 'power'
|
|
base = node.children[0]
|
|
assert base.type == 'name' and base.value == 'hasattr'
|
|
trailer = node.children[1]
|
|
assert trailer.type == 'trailer'
|
|
arglist = trailer.children[1]
|
|
assert arglist.type == 'arglist'
|
|
from jedi.evaluate.param import Arguments
|
|
args = list(Arguments(evaluator, arglist).unpack())
|
|
# Arguments should be very simple
|
|
assert len(args) == 2
|
|
|
|
# Check name
|
|
key, values = args[1]
|
|
assert len(values) == 1
|
|
names = evaluator.eval_element(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])
|
|
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):
|
|
# try/except catch check
|
|
if obj.isinstance(tree.TryStmt) and check_try_for_except(obj, exception):
|
|
return True
|
|
# hasattr check
|
|
if exception == AttributeError and obj.isinstance(tree.IfStmt, tree.WhileStmt):
|
|
if check_hasattr(obj.children[1], obj.children[3]):
|
|
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 check_children(node):
|
|
try:
|
|
children = node.children
|
|
except AttributeError:
|
|
return []
|
|
else:
|
|
nodes = []
|
|
for child in children:
|
|
nodes += check_children(child)
|
|
if child.type == 'trailer':
|
|
c = child.children
|
|
if c[0] == '(' and c[1] != ')':
|
|
if c[1].type != 'arglist':
|
|
if c[1].type == 'argument':
|
|
nodes.append(c[1].children[-1])
|
|
else:
|
|
nodes.append(c[1])
|
|
else:
|
|
for argument in c[1].children:
|
|
if argument.type == 'argument':
|
|
nodes.append(argument.children[-1])
|
|
elif argument.type != 'operator':
|
|
nodes.append(argument)
|
|
return nodes
|
|
|
|
def add_nodes(nodes):
|
|
new = set()
|
|
for node in nodes:
|
|
if isinstance(node, tree.Flow):
|
|
children = node.children
|
|
if node.type == 'for_stmt':
|
|
children = children[2:] # Don't want to include the names.
|
|
# Pick the suite/simple_stmt.
|
|
new |= add_nodes(children)
|
|
elif node.type in ('simple_stmt', 'suite'):
|
|
new |= add_nodes(node.children)
|
|
elif node.type in ('return_stmt', 'yield_expr'):
|
|
try:
|
|
new.add(node.children[1])
|
|
except IndexError:
|
|
pass
|
|
elif node.type not in ('whitespace', 'operator', 'keyword',
|
|
'parameters', 'decorated', 'except_clause') \
|
|
and not isinstance(node, (tree.ClassOrFunc, tree.Import)):
|
|
new.add(node)
|
|
|
|
try:
|
|
children = node.children
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
for next_node in children:
|
|
new.update(check_children(node))
|
|
if next_node.type != 'keyword' and node.type != 'expr_stmt':
|
|
new.add(node)
|
|
return new
|
|
|
|
nodes = set()
|
|
import_names = set()
|
|
decorated_funcs = []
|
|
for scope in module.walk():
|
|
for imp in set(scope.imports):
|
|
import_names |= set(imp.get_defined_names())
|
|
if imp.is_nested():
|
|
import_names |= set(path[-1] for path in imp.paths())
|
|
|
|
children = scope.children
|
|
if isinstance(scope, tree.ClassOrFunc):
|
|
children = children[2:] # We don't want to include the class name.
|
|
nodes |= add_nodes(children)
|
|
|
|
for flow in scope.flows:
|
|
if flow.type == 'for_stmt':
|
|
nodes.add(flow.children[3])
|
|
elif flow.type == 'try_stmt':
|
|
nodes.update(e for e in flow.except_clauses() if e is not None)
|
|
|
|
try:
|
|
decorators = scope.get_decorators()
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if decorators:
|
|
decorated_funcs.append(scope)
|
|
return nodes, import_names, decorated_funcs
|