1
0
forked from VimPlug/jedi

Separate the python syntax tree stuff from the non python stuff.

This commit is contained in:
Dave Halter
2017-03-16 19:54:08 +01:00
parent 448bfd0992
commit 4573ab19f4
4 changed files with 424 additions and 390 deletions

View File

@@ -39,8 +39,10 @@ from itertools import chain
import textwrap
import abc
from jedi._compatibility import (Python3Method, encoding, is_py3, utf8_repr,
from jedi._compatibility import (Python3Method, is_py3, utf8_repr,
literal_eval, unicode)
from jedi.parser.tree import (Node, BaseNode, Leaf, LeafWithNewlines,
ErrorNode, ErrorLeaf)
def _safe_literal_eval(value):
@@ -109,23 +111,7 @@ class DocstringMixin(object):
return ''
class Base(object):
"""
This is just here to have an isinstance check, which is also used on
evaluate classes. But since they have sometimes a special type of
delegation, it is important for those classes to override this method.
I know that there is a chance to do such things with __instancecheck__, but
since Python 2.5 doesn't support it, I decided to do it this way.
"""
__slots__ = ()
def get_root_node(self):
scope = self
while scope.parent is not None:
scope = scope.parent
return scope
class PythonMixin():
def get_parent_scope(self, include_flows=False):
"""
Returns the underlying scope.
@@ -145,7 +131,7 @@ class Base(object):
scope = self
while scope.parent is not None:
parent = scope.parent
if isinstance(scope, (Node, Leaf)) and parent.type != 'simple_stmt':
if isinstance(scope, (PythonNode, PythonLeaf)) and parent.type != 'simple_stmt':
if scope.type == 'testlist_comp':
try:
if isinstance(scope.children[1], CompFor):
@@ -194,161 +180,63 @@ class Base(object):
def nodes_to_execute(self, last_added=False):
raise NotImplementedError()
def get_next_sibling(self):
"""
The node immediately following the invocant in their parent's children
list. If the invocant does not have a next sibling, it is None
"""
# Can't use index(); we need to test by identity
for i, child in enumerate(self.parent.children):
if child is self:
try:
return self.parent.children[i + 1]
except IndexError:
return None
def get_previous_sibling(self):
"""
The node/leaf immediately preceding the invocant in their parent's
children list. If the invocant does not have a previous sibling, it is
None.
"""
# Can't use index(); we need to test by identity
for i, child in enumerate(self.parent.children):
if child is self:
if i == 0:
return None
return self.parent.children[i - 1]
def get_previous_leaf(self):
"""
Returns the previous leaf in the parser tree.
Raises an IndexError if it's the first element.
"""
node = self
while True:
c = node.parent.children
i = c.index(node)
if i == 0:
node = node.parent
if node.parent is None:
return None
@Python3Method
def name_for_position(self, position):
for c in self.children:
if isinstance(c, Leaf):
if isinstance(c, Name) and c.start_pos <= position <= c.end_pos:
return c
else:
node = c[i - 1]
break
result = c.name_for_position(position)
if result is not None:
return result
return None
while True:
try:
node = node.children[-1]
except AttributeError: # A Leaf doesn't have children.
return node
def get_next_leaf(self):
"""
Returns the previous leaf in the parser tree.
Raises an IndexError if it's the last element.
"""
node = self
while True:
c = node.parent.children
i = c.index(node)
if i == len(c) - 1:
node = node.parent
if node.parent is None:
return None
else:
node = c[i + 1]
break
while True:
try:
node = node.children[0]
except AttributeError: # A Leaf doesn't have children.
return node
@Python3Method
def get_statement_for_position(self, pos):
for c in self.children:
if c.start_pos <= pos <= c.end_pos:
if c.type not in ('decorated', 'simple_stmt', 'suite') \
and not isinstance(c, (Flow, ClassOrFunc)):
return c
else:
try:
return c.get_statement_for_position(pos)
except AttributeError:
pass # Must be a non-scope
return None
class Leaf(Base):
__slots__ = ('value', 'parent', 'line', 'indent', 'prefix')
def __init__(self, value, start_pos, prefix=''):
self.value = value
self.start_pos = start_pos
self.prefix = prefix
self.parent = None
@property
def start_pos(self):
return self.line, self.indent
@start_pos.setter
def start_pos(self, value):
self.line = value[0]
self.indent = value[1]
def get_start_pos_of_prefix(self):
previous_leaf = self.get_previous_leaf()
if previous_leaf is None:
return self.line - self.prefix.count('\n'), 0 # It's the first leaf.
return previous_leaf.end_pos
@property
def end_pos(self):
return self.line, self.indent + len(self.value)
def move(self, line_offset):
self.line += line_offset
def get_first_leaf(self):
return self
def get_last_leaf(self):
return self
def get_code(self, normalized=False, include_prefix=True):
if normalized:
return self.value
if include_prefix:
return self.prefix + self.value
else:
return self.value
def nodes_to_execute(self, last_added=False):
return []
@utf8_repr
def __repr__(self):
return "<%s: %s start=%s>" % (type(self).__name__, self.value, self.start_pos)
class PythonLeaf(Leaf, PythonMixin):
pass
class LeafWithNewLines(Leaf):
__slots__ = ()
@property
def end_pos(self):
"""
Literals and whitespace end_pos are more complicated than normal
end_pos, because the containing newlines may change the indexes.
"""
lines = self.value.split('\n')
end_pos_line = self.line + len(lines) - 1
# Check for multiline token
if self.line == end_pos_line:
end_pos_indent = self.indent + len(lines[-1])
else:
end_pos_indent = len(lines[-1])
return end_pos_line, end_pos_indent
@utf8_repr
def __repr__(self):
return "<%s: %r>" % (type(self).__name__, self.value)
class PythonLeafWithNewlines(LeafWithNewlines, PythonMixin):
pass
class EndMarker(Leaf):
class PythonBaseNode(BaseNode, PythonMixin):
pass
class PythonErrorNode(ErrorNode, PythonMixin):
pass
class PythonErrorLeaf(ErrorLeaf, PythonMixin):
pass
class PythonNode(Node, PythonMixin):
pass
class EndMarker(PythonLeaf):
__slots__ = ()
type = 'endmarker'
class Newline(LeafWithNewLines):
class Newline(PythonLeafWithNewlines):
"""Contains NEWLINE and ENDMARKER tokens."""
__slots__ = ()
type = 'newline'
@@ -358,7 +246,7 @@ class Newline(LeafWithNewLines):
return "<%s: %s>" % (type(self).__name__, repr(self.value))
class Name(Leaf):
class Name(PythonLeaf):
"""
A string. Sometimes it is important to know if the string belongs to a name
or not.
@@ -398,7 +286,7 @@ class Name(Leaf):
yield self
class Literal(LeafWithNewLines):
class Literal(PythonLeafWithNewlines):
__slots__ = ()
def eval(self):
@@ -415,7 +303,7 @@ class String(Literal):
__slots__ = ()
class Operator(Leaf):
class Operator(PythonLeaf):
type = 'operator'
__slots__ = ()
@@ -440,7 +328,7 @@ class Operator(Leaf):
return hash(self.value)
class Keyword(Leaf):
class Keyword(PythonLeaf):
type = 'keyword'
__slots__ = ()
@@ -461,215 +349,7 @@ class Keyword(Leaf):
return hash(self.value)
class BaseNode(Base):
"""
The super class for Scope, Import, Name and Statement. Every object in
the parser tree inherits from this class.
"""
__slots__ = ('children', 'parent')
type = None
def __init__(self, children):
"""
Initialize :class:`BaseNode`.
:param children: The module in which this Python object locates.
"""
for c in children:
c.parent = self
self.children = children
self.parent = None
def move(self, line_offset):
"""
Move the Node's start_pos.
"""
for c in self.children:
c.move(line_offset)
@property
def start_pos(self):
return self.children[0].start_pos
def get_start_pos_of_prefix(self):
return self.children[0].get_start_pos_of_prefix()
@property
def end_pos(self):
return self.children[-1].end_pos
def _get_code_for_children(self, children, normalized, include_prefix):
# TODO implement normalized (depending on context).
if include_prefix:
return "".join(c.get_code(normalized) for c in children)
else:
first = children[0].get_code(include_prefix=False)
return first + "".join(c.get_code(normalized) for c in children[1:])
def get_code(self, normalized=False, include_prefix=True):
return self._get_code_for_children(self.children, normalized, include_prefix)
@Python3Method
def name_for_position(self, position):
for c in self.children:
if isinstance(c, Leaf):
if isinstance(c, Name) and c.start_pos <= position <= c.end_pos:
return c
else:
result = c.name_for_position(position)
if result is not None:
return result
return None
def get_leaf_for_position(self, position, include_prefixes=False):
def binary_search(lower, upper):
if lower == upper:
element = self.children[lower]
if not include_prefixes and position < element.start_pos:
# We're on a prefix.
return None
# In case we have prefixes, a leaf always matches
try:
return element.get_leaf_for_position(position, include_prefixes)
except AttributeError:
return element
index = int((lower + upper) / 2)
element = self.children[index]
if position <= element.end_pos:
return binary_search(lower, index)
else:
return binary_search(index + 1, upper)
if not ((1, 0) <= position <= self.children[-1].end_pos):
raise ValueError('Please provide a position that exists within this node.')
return binary_search(0, len(self.children) - 1)
@Python3Method
def get_statement_for_position(self, pos):
for c in self.children:
if c.start_pos <= pos <= c.end_pos:
if c.type not in ('decorated', 'simple_stmt', 'suite') \
and not isinstance(c, (Flow, ClassOrFunc)):
return c
else:
try:
return c.get_statement_for_position(pos)
except AttributeError:
pass # Must be a non-scope
return None
def get_first_leaf(self):
return self.children[0].get_first_leaf()
def get_last_leaf(self):
return self.children[-1].get_last_leaf()
def get_following_comment_same_line(self):
"""
returns (as string) any comment that appears on the same line,
after the node, including the #
"""
try:
if self.type == 'for_stmt':
whitespace = self.children[5].get_first_leaf().prefix
elif self.type == 'with_stmt':
whitespace = self.children[3].get_first_leaf().prefix
else:
whitespace = self.get_last_leaf().get_next_leaf().prefix
except AttributeError:
return None
except ValueError:
# TODO in some particular cases, the tree doesn't seem to be linked
# correctly
return None
if "#" not in whitespace:
return None
comment = whitespace[whitespace.index("#"):]
if "\r" in comment:
comment = comment[:comment.index("\r")]
if "\n" in comment:
comment = comment[:comment.index("\n")]
return comment
@utf8_repr
def __repr__(self):
code = self.get_code().replace('\n', ' ').strip()
if not is_py3:
code = code.encode(encoding, 'replace')
return "<%s: %s@%s,%s>" % \
(type(self).__name__, code, self.start_pos[0], self.start_pos[1])
class Node(BaseNode):
"""Concrete implementation for interior nodes."""
__slots__ = ('type',)
_IGNORE_EXECUTE_NODES = set([
'suite', 'subscriptlist', 'subscript', 'simple_stmt', 'sliceop',
'testlist_comp', 'dictorsetmaker', 'trailer', 'decorators',
'decorated', 'arglist', 'argument', 'exprlist', 'testlist',
'testlist_safe', 'testlist1'
])
def __init__(self, type, children):
"""
Initializer.
Takes a type constant (a symbol number >= 256), a sequence of
child nodes, and an optional context keyword argument.
As a side effect, the parent pointers of the children are updated.
"""
super(Node, self).__init__(children)
self.type = type
def nodes_to_execute(self, last_added=False):
"""
For static analysis.
"""
result = []
if self.type not in Node._IGNORE_EXECUTE_NODES and not last_added:
result.append(self)
last_added = True
for child in self.children:
result += child.nodes_to_execute(last_added)
return result
def __repr__(self):
return "%s(%s, %r)" % (self.__class__.__name__, self.type, self.children)
class ErrorNode(BaseNode):
"""
TODO doc
"""
__slots__ = ()
type = 'error_node'
def nodes_to_execute(self, last_added=False):
return []
class ErrorLeaf(LeafWithNewLines):
"""
TODO doc
"""
__slots__ = ('original_type')
type = 'error_leaf'
def __init__(self, original_type, value, start_pos, prefix=''):
super(ErrorLeaf, self).__init__(value, start_pos, prefix)
self.original_type = original_type
def __repr__(self):
return "<%s: %s:%s, %s)>" % \
(type(self).__name__, self.original_type, repr(self.value), self.start_pos)
class Scope(BaseNode, DocstringMixin):
class Scope(PythonBaseNode, DocstringMixin):
"""
Super class for the parser tree, which represents the state of a python
text file.
@@ -811,7 +491,7 @@ class Module(Scope):
return result
class Decorator(BaseNode):
class Decorator(PythonBaseNode):
type = 'decorator'
__slots__ = ()
@@ -1097,7 +777,7 @@ class Lambda(Function):
return "<%s@%s>" % (self.__class__.__name__, self.start_pos)
class Flow(BaseNode):
class Flow(PythonBaseNode):
__slots__ = ()
FLOW_KEYWORDS = (
'try', 'except', 'finally', 'else', 'if', 'elif', 'with', 'for', 'while'
@@ -1249,7 +929,7 @@ class WithStmt(Flow):
return result
class Import(BaseNode):
class Import(PythonBaseNode):
__slots__ = ()
def path_for_name(self, name):
@@ -1399,7 +1079,7 @@ class ImportName(Import):
if alias is not None)
class KeywordStatement(BaseNode):
class KeywordStatement(PythonBaseNode):
"""
For the following statements: `assert`, `del`, `global`, `nonlocal`,
`raise`, `return`, `yield`, `return`, `yield`.
@@ -1456,7 +1136,7 @@ class ReturnStmt(KeywordStatement):
__slots__ = ()
class YieldExpr(BaseNode):
class YieldExpr(PythonBaseNode):
__slots__ = ()
@property
@@ -1491,7 +1171,7 @@ def _defined_names(current):
return names
class ExprStmt(BaseNode, DocstringMixin):
class ExprStmt(PythonBaseNode, DocstringMixin):
type = 'expr_stmt'
__slots__ = ()
@@ -1527,7 +1207,7 @@ class ExprStmt(BaseNode, DocstringMixin):
return result
class Param(BaseNode):
class Param(PythonBaseNode):
"""
It's a helper class that makes business logic with params much easier. The
Python grammar defines no ``param`` node. It defines it in a different way
@@ -1597,7 +1277,7 @@ class Param(BaseNode):
return self._get_code_for_children(children, False, False)
class CompFor(BaseNode):
class CompFor(PythonBaseNode):
type = 'comp_for'
__slots__ = ()