You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
666 lines
22 KiB
Python
666 lines
22 KiB
Python
# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
|
|
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
|
#
|
|
# This file is part of astroid.
|
|
#
|
|
# astroid is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Lesser General Public License as published by the
|
|
# Free Software Foundation, either version 2.1 of the License, or (at your
|
|
# option) any later version.
|
|
#
|
|
# astroid is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
# for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License along
|
|
# with astroid. If not, see <http://www.gnu.org/licenses/>.
|
|
"""This module contains base classes and functions for the nodes and some
|
|
inference utils.
|
|
"""
|
|
|
|
__docformat__ = "restructuredtext en"
|
|
|
|
import sys
|
|
from contextlib import contextmanager
|
|
|
|
from logilab.common.decorators import cachedproperty
|
|
|
|
from astroid.exceptions import (InferenceError, AstroidError, NotFoundError,
|
|
UnresolvableName, UseInferenceDefault)
|
|
|
|
|
|
if sys.version_info >= (3, 0):
|
|
BUILTINS = 'builtins'
|
|
else:
|
|
BUILTINS = '__builtin__'
|
|
|
|
|
|
class Proxy(object):
|
|
"""a simple proxy object"""
|
|
|
|
_proxied = None # proxied object may be set by class or by instance
|
|
|
|
def __init__(self, proxied=None):
|
|
if proxied is not None:
|
|
self._proxied = proxied
|
|
|
|
def __getattr__(self, name):
|
|
if name == '_proxied':
|
|
return getattr(self.__class__, '_proxied')
|
|
if name in self.__dict__:
|
|
return self.__dict__[name]
|
|
return getattr(self._proxied, name)
|
|
|
|
def infer(self, context=None):
|
|
yield self
|
|
|
|
|
|
# Inference ##################################################################
|
|
|
|
MISSING = object()
|
|
|
|
|
|
class InferenceContext(object):
|
|
__slots__ = ('path', 'callcontext', 'boundnode', 'infered')
|
|
|
|
def __init__(self,
|
|
path=None, callcontext=None, boundnode=None, infered=None):
|
|
if path is None:
|
|
self.path = frozenset()
|
|
else:
|
|
self.path = path
|
|
self.callcontext = callcontext
|
|
self.boundnode = boundnode
|
|
if infered is None:
|
|
self.infered = {}
|
|
else:
|
|
self.infered = infered
|
|
|
|
def push(self, key):
|
|
# This returns a NEW context with the same attributes, but a new key
|
|
# added to `path`. The intention is that it's only passed to callees
|
|
# and then destroyed; otherwise scope() may not work correctly.
|
|
# The cache will be shared, since it's the same exact dict.
|
|
if key in self.path:
|
|
# End the containing generator
|
|
raise StopIteration
|
|
|
|
return InferenceContext(
|
|
self.path.union([key]),
|
|
self.callcontext,
|
|
self.boundnode,
|
|
self.infered,
|
|
)
|
|
|
|
@contextmanager
|
|
def scope(self, callcontext=MISSING, boundnode=MISSING):
|
|
try:
|
|
orig = self.callcontext, self.boundnode
|
|
if callcontext is not MISSING:
|
|
self.callcontext = callcontext
|
|
if boundnode is not MISSING:
|
|
self.boundnode = boundnode
|
|
yield
|
|
finally:
|
|
self.callcontext, self.boundnode = orig
|
|
|
|
def cache_generator(self, key, generator):
|
|
results = []
|
|
for result in generator:
|
|
results.append(result)
|
|
yield result
|
|
|
|
self.infered[key] = tuple(results)
|
|
return
|
|
|
|
|
|
def _infer_stmts(stmts, context, frame=None, lookupname=None):
|
|
"""return an iterator on statements inferred by each statement in <stmts>
|
|
"""
|
|
stmt = None
|
|
infered = False
|
|
if context is None:
|
|
context = InferenceContext()
|
|
for stmt in stmts:
|
|
if stmt is YES:
|
|
yield stmt
|
|
infered = True
|
|
continue
|
|
|
|
kw = {}
|
|
infered_name = stmt._infer_name(frame, lookupname)
|
|
if infered_name is not None:
|
|
# only returns not None if .infer() accepts a lookupname kwarg
|
|
kw['lookupname'] = infered_name
|
|
|
|
try:
|
|
for infered in stmt.infer(context, **kw):
|
|
yield infered
|
|
infered = True
|
|
except UnresolvableName:
|
|
continue
|
|
except InferenceError:
|
|
yield YES
|
|
infered = True
|
|
if not infered:
|
|
raise InferenceError(str(stmt))
|
|
|
|
|
|
# special inference objects (e.g. may be returned as nodes by .infer()) #######
|
|
|
|
class _Yes(object):
|
|
"""a yes object"""
|
|
def __repr__(self):
|
|
return 'YES'
|
|
def __getattribute__(self, name):
|
|
if name == 'next':
|
|
raise AttributeError('next method should not be called')
|
|
if name.startswith('__') and name.endswith('__'):
|
|
# to avoid inspection pb
|
|
return super(_Yes, self).__getattribute__(name)
|
|
return self
|
|
def __call__(self, *args, **kwargs):
|
|
return self
|
|
|
|
|
|
YES = _Yes()
|
|
|
|
|
|
class Instance(Proxy):
|
|
"""a special node representing a class instance"""
|
|
def getattr(self, name, context=None, lookupclass=True):
|
|
try:
|
|
values = self._proxied.instance_attr(name, context)
|
|
except NotFoundError:
|
|
if name == '__class__':
|
|
return [self._proxied]
|
|
if lookupclass:
|
|
# class attributes not available through the instance
|
|
# unless they are explicitly defined
|
|
if name in ('__name__', '__bases__', '__mro__', '__subclasses__'):
|
|
return self._proxied.local_attr(name)
|
|
return self._proxied.getattr(name, context)
|
|
raise NotFoundError(name)
|
|
# since we've no context information, return matching class members as
|
|
# well
|
|
if lookupclass:
|
|
try:
|
|
return values + self._proxied.getattr(name, context)
|
|
except NotFoundError:
|
|
pass
|
|
return values
|
|
|
|
def igetattr(self, name, context=None):
|
|
"""inferred getattr"""
|
|
if not context:
|
|
context = InferenceContext()
|
|
try:
|
|
# avoid recursively inferring the same attr on the same class
|
|
new_context = context.push((self._proxied, name))
|
|
# XXX frame should be self._proxied, or not ?
|
|
get_attr = self.getattr(name, new_context, lookupclass=False)
|
|
return _infer_stmts(
|
|
self._wrap_attr(get_attr, new_context),
|
|
new_context,
|
|
frame=self,
|
|
)
|
|
except NotFoundError:
|
|
try:
|
|
# fallback to class'igetattr since it has some logic to handle
|
|
# descriptors
|
|
return self._wrap_attr(self._proxied.igetattr(name, context),
|
|
context)
|
|
except NotFoundError:
|
|
raise InferenceError(name)
|
|
|
|
def _wrap_attr(self, attrs, context=None):
|
|
"""wrap bound methods of attrs in a InstanceMethod proxies"""
|
|
for attr in attrs:
|
|
if isinstance(attr, UnboundMethod):
|
|
if BUILTINS + '.property' in attr.decoratornames():
|
|
for infered in attr.infer_call_result(self, context):
|
|
yield infered
|
|
else:
|
|
yield BoundMethod(attr, self)
|
|
else:
|
|
yield attr
|
|
|
|
def infer_call_result(self, caller, context=None):
|
|
"""infer what a class instance is returning when called"""
|
|
infered = False
|
|
for node in self._proxied.igetattr('__call__', context):
|
|
if node is YES:
|
|
continue
|
|
for res in node.infer_call_result(caller, context):
|
|
infered = True
|
|
yield res
|
|
if not infered:
|
|
raise InferenceError()
|
|
|
|
def __repr__(self):
|
|
return '<Instance of %s.%s at 0x%s>' % (self._proxied.root().name,
|
|
self._proxied.name,
|
|
id(self))
|
|
def __str__(self):
|
|
return 'Instance of %s.%s' % (self._proxied.root().name,
|
|
self._proxied.name)
|
|
|
|
def callable(self):
|
|
try:
|
|
self._proxied.getattr('__call__')
|
|
return True
|
|
except NotFoundError:
|
|
return False
|
|
|
|
def pytype(self):
|
|
return self._proxied.qname()
|
|
|
|
def display_type(self):
|
|
return 'Instance of'
|
|
|
|
|
|
class UnboundMethod(Proxy):
|
|
"""a special node representing a method not bound to an instance"""
|
|
def __repr__(self):
|
|
frame = self._proxied.parent.frame()
|
|
return '<%s %s of %s at 0x%s' % (self.__class__.__name__,
|
|
self._proxied.name,
|
|
frame.qname(), id(self))
|
|
|
|
def is_bound(self):
|
|
return False
|
|
|
|
def getattr(self, name, context=None):
|
|
if name == 'im_func':
|
|
return [self._proxied]
|
|
return super(UnboundMethod, self).getattr(name, context)
|
|
|
|
def igetattr(self, name, context=None):
|
|
if name == 'im_func':
|
|
return iter((self._proxied,))
|
|
return super(UnboundMethod, self).igetattr(name, context)
|
|
|
|
def infer_call_result(self, caller, context):
|
|
# If we're unbound method __new__ of builtin object, the result is an
|
|
# instance of the class given as first argument.
|
|
if (self._proxied.name == '__new__' and
|
|
self._proxied.parent.frame().qname() == '%s.object' % BUILTINS):
|
|
infer = caller.args[0].infer() if caller.args else []
|
|
return ((x is YES and x or Instance(x)) for x in infer)
|
|
return self._proxied.infer_call_result(caller, context)
|
|
|
|
|
|
class BoundMethod(UnboundMethod):
|
|
"""a special node representing a method bound to an instance"""
|
|
def __init__(self, proxy, bound):
|
|
UnboundMethod.__init__(self, proxy)
|
|
self.bound = bound
|
|
|
|
def is_bound(self):
|
|
return True
|
|
|
|
def infer_call_result(self, caller, context):
|
|
with context.scope(boundnode=self.bound):
|
|
for infered in self._proxied.infer_call_result(caller, context):
|
|
yield infered
|
|
|
|
|
|
class Generator(Instance):
|
|
"""a special node representing a generator.
|
|
|
|
Proxied class is set once for all in raw_building.
|
|
"""
|
|
def callable(self):
|
|
return False
|
|
|
|
def pytype(self):
|
|
return '%s.generator' % BUILTINS
|
|
|
|
def display_type(self):
|
|
return 'Generator'
|
|
|
|
def __repr__(self):
|
|
return '<Generator(%s) l.%s at 0x%s>' % (self._proxied.name, self.lineno, id(self))
|
|
|
|
def __str__(self):
|
|
return 'Generator(%s)' % (self._proxied.name)
|
|
|
|
|
|
# decorators ##################################################################
|
|
|
|
def path_wrapper(func):
|
|
"""return the given infer function wrapped to handle the path"""
|
|
def wrapped(node, context=None, _func=func, **kwargs):
|
|
"""wrapper function handling context"""
|
|
if context is None:
|
|
context = InferenceContext()
|
|
context = context.push((node, kwargs.get('lookupname')))
|
|
|
|
yielded = set()
|
|
for res in _func(node, context, **kwargs):
|
|
# unproxy only true instance, not const, tuple, dict...
|
|
if res.__class__ is Instance:
|
|
ares = res._proxied
|
|
else:
|
|
ares = res
|
|
if not ares in yielded:
|
|
yield res
|
|
yielded.add(ares)
|
|
return wrapped
|
|
|
|
def yes_if_nothing_infered(func):
|
|
def wrapper(*args, **kwargs):
|
|
infered = False
|
|
for node in func(*args, **kwargs):
|
|
infered = True
|
|
yield node
|
|
if not infered:
|
|
yield YES
|
|
return wrapper
|
|
|
|
def raise_if_nothing_infered(func):
|
|
def wrapper(*args, **kwargs):
|
|
infered = False
|
|
for node in func(*args, **kwargs):
|
|
infered = True
|
|
yield node
|
|
if not infered:
|
|
raise InferenceError()
|
|
return wrapper
|
|
|
|
|
|
# Node ######################################################################
|
|
|
|
class NodeNG(object):
|
|
"""Base Class for all Astroid node classes.
|
|
|
|
It represents a node of the new abstract syntax tree.
|
|
"""
|
|
is_statement = False
|
|
optional_assign = False # True for For (and for Comprehension if py <3.0)
|
|
is_function = False # True for Function nodes
|
|
# attributes below are set by the builder module or by raw factories
|
|
lineno = None
|
|
fromlineno = None
|
|
tolineno = None
|
|
col_offset = None
|
|
# parent node in the tree
|
|
parent = None
|
|
# attributes containing child node(s) redefined in most concrete classes:
|
|
_astroid_fields = ()
|
|
# instance specific inference function infer(node, context)
|
|
_explicit_inference = None
|
|
|
|
def infer(self, context=None, **kwargs):
|
|
"""main interface to the interface system, return a generator on infered
|
|
values.
|
|
|
|
If the instance has some explicit inference function set, it will be
|
|
called instead of the default interface.
|
|
"""
|
|
if self._explicit_inference is not None:
|
|
# explicit_inference is not bound, give it self explicitly
|
|
try:
|
|
return self._explicit_inference(self, context, **kwargs)
|
|
except UseInferenceDefault:
|
|
pass
|
|
|
|
if not context:
|
|
return self._infer(context, **kwargs)
|
|
|
|
key = (self, kwargs.get('lookupname'), context.callcontext, context.boundnode)
|
|
if key in context.infered:
|
|
return iter(context.infered[key])
|
|
|
|
return context.cache_generator(key, self._infer(context, **kwargs))
|
|
|
|
def _repr_name(self):
|
|
"""return self.name or self.attrname or '' for nice representation"""
|
|
return getattr(self, 'name', getattr(self, 'attrname', ''))
|
|
|
|
def __str__(self):
|
|
return '%s(%s)' % (self.__class__.__name__, self._repr_name())
|
|
|
|
def __repr__(self):
|
|
return '<%s(%s) l.%s [%s] at 0x%x>' % (self.__class__.__name__,
|
|
self._repr_name(),
|
|
self.fromlineno,
|
|
self.root().name,
|
|
id(self))
|
|
|
|
|
|
def accept(self, visitor):
|
|
func = getattr(visitor, "visit_" + self.__class__.__name__.lower())
|
|
return func(self)
|
|
|
|
def get_children(self):
|
|
for field in self._astroid_fields:
|
|
attr = getattr(self, field)
|
|
if attr is None:
|
|
continue
|
|
if isinstance(attr, (list, tuple)):
|
|
for elt in attr:
|
|
yield elt
|
|
else:
|
|
yield attr
|
|
|
|
def last_child(self):
|
|
"""an optimized version of list(get_children())[-1]"""
|
|
for field in self._astroid_fields[::-1]:
|
|
attr = getattr(self, field)
|
|
if not attr: # None or empty listy / tuple
|
|
continue
|
|
if attr.__class__ in (list, tuple):
|
|
return attr[-1]
|
|
else:
|
|
return attr
|
|
return None
|
|
|
|
def parent_of(self, node):
|
|
"""return true if i'm a parent of the given node"""
|
|
parent = node.parent
|
|
while parent is not None:
|
|
if self is parent:
|
|
return True
|
|
parent = parent.parent
|
|
return False
|
|
|
|
def statement(self):
|
|
"""return the first parent node marked as statement node"""
|
|
if self.is_statement:
|
|
return self
|
|
return self.parent.statement()
|
|
|
|
def frame(self):
|
|
"""return the first parent frame node (i.e. Module, Function or Class)
|
|
"""
|
|
return self.parent.frame()
|
|
|
|
def scope(self):
|
|
"""return the first node defining a new scope (i.e. Module, Function,
|
|
Class, Lambda but also GenExpr)
|
|
"""
|
|
return self.parent.scope()
|
|
|
|
def root(self):
|
|
"""return the root node of the tree, (i.e. a Module)"""
|
|
if self.parent:
|
|
return self.parent.root()
|
|
return self
|
|
|
|
def child_sequence(self, child):
|
|
"""search for the right sequence where the child lies in"""
|
|
for field in self._astroid_fields:
|
|
node_or_sequence = getattr(self, field)
|
|
if node_or_sequence is child:
|
|
return [node_or_sequence]
|
|
# /!\ compiler.ast Nodes have an __iter__ walking over child nodes
|
|
if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence:
|
|
return node_or_sequence
|
|
else:
|
|
msg = 'Could not find %s in %s\'s children'
|
|
raise AstroidError(msg % (repr(child), repr(self)))
|
|
|
|
def locate_child(self, child):
|
|
"""return a 2-uple (child attribute name, sequence or node)"""
|
|
for field in self._astroid_fields:
|
|
node_or_sequence = getattr(self, field)
|
|
# /!\ compiler.ast Nodes have an __iter__ walking over child nodes
|
|
if child is node_or_sequence:
|
|
return field, child
|
|
if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence:
|
|
return field, node_or_sequence
|
|
msg = 'Could not find %s in %s\'s children'
|
|
raise AstroidError(msg % (repr(child), repr(self)))
|
|
# FIXME : should we merge child_sequence and locate_child ? locate_child
|
|
# is only used in are_exclusive, child_sequence one time in pylint.
|
|
|
|
def next_sibling(self):
|
|
"""return the next sibling statement"""
|
|
return self.parent.next_sibling()
|
|
|
|
def previous_sibling(self):
|
|
"""return the previous sibling statement"""
|
|
return self.parent.previous_sibling()
|
|
|
|
def nearest(self, nodes):
|
|
"""return the node which is the nearest before this one in the
|
|
given list of nodes
|
|
"""
|
|
myroot = self.root()
|
|
mylineno = self.fromlineno
|
|
nearest = None, 0
|
|
for node in nodes:
|
|
assert node.root() is myroot, \
|
|
'nodes %s and %s are not from the same module' % (self, node)
|
|
lineno = node.fromlineno
|
|
if node.fromlineno > mylineno:
|
|
break
|
|
if lineno > nearest[1]:
|
|
nearest = node, lineno
|
|
# FIXME: raise an exception if nearest is None ?
|
|
return nearest[0]
|
|
|
|
# these are lazy because they're relatively expensive to compute for every
|
|
# single node, and they rarely get looked at
|
|
|
|
@cachedproperty
|
|
def fromlineno(self):
|
|
if self.lineno is None:
|
|
return self._fixed_source_line()
|
|
else:
|
|
return self.lineno
|
|
|
|
@cachedproperty
|
|
def tolineno(self):
|
|
if not self._astroid_fields:
|
|
# can't have children
|
|
lastchild = None
|
|
else:
|
|
lastchild = self.last_child()
|
|
if lastchild is None:
|
|
return self.fromlineno
|
|
else:
|
|
return lastchild.tolineno
|
|
|
|
# TODO / FIXME:
|
|
assert self.fromlineno is not None, self
|
|
assert self.tolineno is not None, self
|
|
|
|
def _fixed_source_line(self):
|
|
"""return the line number where the given node appears
|
|
|
|
we need this method since not all nodes have the lineno attribute
|
|
correctly set...
|
|
"""
|
|
line = self.lineno
|
|
_node = self
|
|
try:
|
|
while line is None:
|
|
_node = next(_node.get_children())
|
|
line = _node.lineno
|
|
except StopIteration:
|
|
_node = self.parent
|
|
while _node and line is None:
|
|
line = _node.lineno
|
|
_node = _node.parent
|
|
return line
|
|
|
|
def block_range(self, lineno):
|
|
"""handle block line numbers range for non block opening statements
|
|
"""
|
|
return lineno, self.tolineno
|
|
|
|
def set_local(self, name, stmt):
|
|
"""delegate to a scoped parent handling a locals dictionary"""
|
|
self.parent.set_local(name, stmt)
|
|
|
|
def nodes_of_class(self, klass, skip_klass=None):
|
|
"""return an iterator on nodes which are instance of the given class(es)
|
|
|
|
klass may be a class object or a tuple of class objects
|
|
"""
|
|
if isinstance(self, klass):
|
|
yield self
|
|
for child_node in self.get_children():
|
|
if skip_klass is not None and isinstance(child_node, skip_klass):
|
|
continue
|
|
for matching in child_node.nodes_of_class(klass, skip_klass):
|
|
yield matching
|
|
|
|
def _infer_name(self, frame, name):
|
|
# overridden for From, Import, Global, TryExcept and Arguments
|
|
return None
|
|
|
|
def _infer(self, context=None):
|
|
"""we don't know how to resolve a statement by default"""
|
|
# this method is overridden by most concrete classes
|
|
raise InferenceError(self.__class__.__name__)
|
|
|
|
def infered(self):
|
|
'''return list of infered values for a more simple inference usage'''
|
|
return list(self.infer())
|
|
|
|
def instanciate_class(self):
|
|
"""instanciate a node if it is a Class node, else return self"""
|
|
return self
|
|
|
|
def has_base(self, node):
|
|
return False
|
|
|
|
def callable(self):
|
|
return False
|
|
|
|
def eq(self, value):
|
|
return False
|
|
|
|
def as_string(self):
|
|
from astroid.as_string import to_code
|
|
return to_code(self)
|
|
|
|
def repr_tree(self, ids=False):
|
|
from astroid.as_string import dump
|
|
return dump(self)
|
|
|
|
|
|
class Statement(NodeNG):
|
|
"""Statement node adding a few attributes"""
|
|
is_statement = True
|
|
|
|
def next_sibling(self):
|
|
"""return the next sibling statement"""
|
|
stmts = self.parent.child_sequence(self)
|
|
index = stmts.index(self)
|
|
try:
|
|
return stmts[index +1]
|
|
except IndexError:
|
|
pass
|
|
|
|
def previous_sibling(self):
|
|
"""return the previous sibling statement"""
|
|
stmts = self.parent.child_sequence(self)
|
|
index = stmts.index(self)
|
|
if index >= 1:
|
|
return stmts[index -1]
|