Source code for astroid.nodes.node_ng

# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt

from __future__ import annotations

import pprint
import sys
import warnings
from collections.abc import Generator, Iterator
from functools import cached_property
from functools import singledispatch as _singledispatch
from typing import (
    TYPE_CHECKING,
    Any,
    ClassVar,
    Literal,
    Tuple,
    Type,
    TypeVar,
    Union,
    cast,
    overload,
)

from astroid import util
from astroid.context import InferenceContext
from astroid.exceptions import (
    AstroidError,
    InferenceError,
    ParentMissingError,
    StatementMissing,
    UseInferenceDefault,
)
from astroid.manager import AstroidManager
from astroid.nodes.as_string import AsStringVisitor
from astroid.nodes.const import OP_PRECEDENCE
from astroid.nodes.utils import Position
from astroid.typing import InferenceErrorInfo, InferenceResult, InferFn

if sys.version_info >= (3, 11):
    from typing import Self
else:
    from typing_extensions import Self


if TYPE_CHECKING:
    from astroid import nodes
    from astroid.nodes import _base_nodes


# Types for 'NodeNG.nodes_of_class()'
_NodesT = TypeVar("_NodesT", bound="NodeNG")
_NodesT2 = TypeVar("_NodesT2", bound="NodeNG")
_NodesT3 = TypeVar("_NodesT3", bound="NodeNG")
SkipKlassT = Union[None, Type["NodeNG"], Tuple[Type["NodeNG"], ...]]


[docs] class NodeNG: """A node of the new Abstract Syntax Tree (AST). This is the base class for all Astroid node classes. """ is_statement: ClassVar[bool] = False """Whether this node indicates a statement.""" optional_assign: ClassVar[bool] = ( False # True for For (and for Comprehension if py <3.0) ) """Whether this node optionally assigns a variable. This is for loop assignments because loop won't necessarily perform an assignment if the loop has no iterations. This is also the case from comprehensions in Python 2. """ is_function: ClassVar[bool] = False # True for FunctionDef nodes """Whether this node indicates a function.""" is_lambda: ClassVar[bool] = False # Attributes below are set by the builder module or by raw factories _astroid_fields: ClassVar[tuple[str, ...]] = () """Node attributes that contain child nodes. This is redefined in most concrete classes. """ _other_fields: ClassVar[tuple[str, ...]] = () """Node attributes that do not contain child nodes.""" _other_other_fields: ClassVar[tuple[str, ...]] = () """Attributes that contain AST-dependent fields.""" # instance specific inference function infer(node, context) _explicit_inference: InferFn[Self] | None = None def __init__( self, lineno: int | None, col_offset: int | None, parent: NodeNG | None, *, end_lineno: int | None, end_col_offset: int | None, ) -> None: self.lineno = lineno """The line that this node appears on in the source code.""" self.col_offset = col_offset """The column that this node appears on in the source code.""" self.parent = parent """The parent node in the syntax tree.""" self.end_lineno = end_lineno """The last line this node appears on in the source code.""" self.end_col_offset = end_col_offset """The end column this node appears on in the source code. Note: This is after the last symbol. """ self.position: Position | None = None """Position of keyword(s) and name. Used as fallback for block nodes which might not provide good enough positional information. E.g. ClassDef, FunctionDef. """
[docs] def infer( self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[InferenceResult, None, None]: """Get a generator of the inferred values. This is the main entry point to the inference system. .. seealso:: :ref:`inference` If the instance has some explicit inference function set, it will be called instead of the default interface. :returns: The inferred values. :rtype: iterable """ if context is None: context = InferenceContext() else: context = context.extra_context.get(self, context) if self._explicit_inference is not None: # explicit_inference is not bound, give it self explicitly try: for result in self._explicit_inference( self, # type: ignore[arg-type] context, **kwargs, ): context.nodes_inferred += 1 yield result return except UseInferenceDefault: pass key = (self, context.lookupname, context.callcontext, context.boundnode) if key in context.inferred: yield from context.inferred[key] return results = [] # Limit inference amount to help with performance issues with # exponentially exploding possible results. limit = AstroidManager().max_inferable_values for i, result in enumerate(self._infer(context=context, **kwargs)): if i >= limit or (context.nodes_inferred > context.max_inferred): results.append(util.Uninferable) yield util.Uninferable break results.append(result) yield result context.nodes_inferred += 1 # Cache generated results for subsequent inferences of the # same node using the same context context.inferred[key] = tuple(results) return
[docs] def repr_name(self) -> str: """Get a name for nice representation. This is either :attr:`name`, :attr:`attrname`, or the empty string. """ if all(name not in self._astroid_fields for name in ("name", "attrname")): return getattr(self, "name", "") or getattr(self, "attrname", "") return ""
def __str__(self) -> str: rname = self.repr_name() cname = type(self).__name__ if rname: string = "%(cname)s.%(rname)s(%(fields)s)" alignment = len(cname) + len(rname) + 2 else: string = "%(cname)s(%(fields)s)" alignment = len(cname) + 1 result = [] for field in self._other_fields + self._astroid_fields: value = getattr(self, field, "Unknown") width = 80 - len(field) - alignment lines = pprint.pformat(value, indent=2, width=width).splitlines(True) inner = [lines[0]] for line in lines[1:]: inner.append(" " * alignment + line) result.append(f"{field}={''.join(inner)}") return string % { "cname": cname, "rname": rname, "fields": (",\n" + " " * alignment).join(result), } def __repr__(self) -> str: rname = self.repr_name() # The dependencies used to calculate fromlineno (if not cached) may not exist at the time try: lineno = self.fromlineno except AttributeError: lineno = 0 if rname: string = "<%(cname)s.%(rname)s l.%(lineno)s at 0x%(id)x>" else: string = "<%(cname)s l.%(lineno)s at 0x%(id)x>" return string % { "cname": type(self).__name__, "rname": rname, "lineno": lineno, "id": id(self), }
[docs] def accept(self, visitor: AsStringVisitor) -> str: """Visit this node using the given visitor.""" func = getattr(visitor, "visit_" + self.__class__.__name__.lower()) return func(self)
[docs] def get_children(self) -> Iterator[NodeNG]: """Get the child nodes below this node.""" for field in self._astroid_fields: attr = getattr(self, field) if attr is None: continue if isinstance(attr, (list, tuple)): yield from attr else: yield attr yield from ()
[docs] def last_child(self) -> NodeNG | None: """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 list / tuple continue if isinstance(attr, (list, tuple)): return attr[-1] return attr return None
[docs] def node_ancestors(self) -> Iterator[NodeNG]: """Yield parent, grandparent, etc until there are no more.""" parent = self.parent while parent is not None: yield parent parent = parent.parent
[docs] def parent_of(self, node) -> bool: """Check if this node is the parent of the given node. :param node: The node to check if it is the child. :type node: NodeNG :returns: Whether this node is the parent of the given node. """ return any(self is parent for parent in node.node_ancestors())
[docs] def statement(self, *, future: Literal[None, True] = None) -> _base_nodes.Statement: """The first parent node, including self, marked as statement node. :raises StatementMissing: If self has no parent attribute. """ if future is not None: warnings.warn( "The future arg will be removed in astroid 4.0.", DeprecationWarning, stacklevel=2, ) if self.is_statement: return cast("_base_nodes.Statement", self) if not self.parent: raise StatementMissing(target=self) return self.parent.statement()
[docs] def frame( self, *, future: Literal[None, True] = None ) -> nodes.FunctionDef | nodes.Module | nodes.ClassDef | nodes.Lambda: """The first parent frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, :class:`ClassDef` or :class:`Lambda`. :returns: The first parent frame node. :raises ParentMissingError: If self has no parent attribute. """ if future is not None: warnings.warn( "The future arg will be removed in astroid 4.0.", DeprecationWarning, stacklevel=2, ) if self.parent is None: raise ParentMissingError(target=self) return self.parent.frame(future=future)
[docs] def scope(self) -> nodes.LocalsDictNodeNG: """The first parent node defining a new scope. These can be Module, FunctionDef, ClassDef, Lambda, or GeneratorExp nodes. :returns: The first parent scope node. """ if not self.parent: raise ParentMissingError(target=self) return self.parent.scope()
[docs] def root(self) -> nodes.Module: """Return the root node of the syntax tree. :returns: The root node. """ if not (parent := self.parent): return self # type: ignore[return-value] # Only 'Module' does not have a parent node. while parent.parent: parent = parent.parent return parent # type: ignore[return-value] # Only 'Module' does not have a parent node.
[docs] def child_sequence(self, child): """Search for the sequence that contains this child. :param child: The child node to search sequences for. :type child: NodeNG :returns: The sequence containing the given child node. :rtype: iterable(NodeNG) :raises AstroidError: If no sequence could be found that contains the given child. """ 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 msg = "Could not find %s in %s's children" raise AstroidError(msg % (repr(child), repr(self)))
[docs] def locate_child(self, child): """Find the field of this node that contains the given child. :param child: The child node to search fields for. :type child: NodeNG :returns: A tuple of the name of the field that contains the child, and the sequence or node that contains the child node. :rtype: tuple(str, iterable(NodeNG) or NodeNG) :raises AstroidError: If no field could be found that contains the given child. """ 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.
[docs] def next_sibling(self): """The next sibling statement node. :returns: The next sibling statement node. :rtype: NodeNG or None """ return self.parent.next_sibling()
[docs] def previous_sibling(self): """The previous sibling statement. :returns: The previous sibling statement node. :rtype: NodeNG or None """ return self.parent.previous_sibling()
# these are lazy because they're relatively expensive to compute for every # single node, and they rarely get looked at @cached_property def fromlineno(self) -> int: """The first line that this node appears on in the source code. Can also return 0 if the line can not be determined. """ if self.lineno is None: return self._fixed_source_line() return self.lineno @cached_property def tolineno(self) -> int: """The last line that this node appears on in the source code. Can also return 0 if the line can not be determined. """ if self.end_lineno is not None: return self.end_lineno if not self._astroid_fields: # can't have children last_child = None else: last_child = self.last_child() if last_child is None: return self.fromlineno return last_child.tolineno def _fixed_source_line(self) -> int: """Attempt to find the line that this node appears on. We need this method since not all nodes have :attr:`lineno` set. Will return 0 if the line number can not be determined. """ line = self.lineno _node = self try: while line is None: _node = next(_node.get_children()) line = _node.lineno except StopIteration: parent = self.parent while parent and line is None: line = parent.lineno parent = parent.parent return line or 0
[docs] def block_range(self, lineno: int) -> tuple[int, int]: """Get a range from the given line number to where this node ends. :param lineno: The line number to start the range at. :returns: The range of line numbers that this node belongs to, starting at the given line number. """ return lineno, self.tolineno
[docs] def set_local(self, name: str, stmt: NodeNG) -> None: """Define that the given name is declared in the given statement node. This definition is stored on the parent scope node. .. seealso:: :meth:`scope` :param name: The name that is being defined. :param stmt: The statement that defines the given name. """ assert self.parent self.parent.set_local(name, stmt)
@overload def nodes_of_class( self, klass: type[_NodesT], skip_klass: SkipKlassT = ..., ) -> Iterator[_NodesT]: ... @overload def nodes_of_class( self, klass: tuple[type[_NodesT], type[_NodesT2]], skip_klass: SkipKlassT = ..., ) -> Iterator[_NodesT] | Iterator[_NodesT2]: ... @overload def nodes_of_class( self, klass: tuple[type[_NodesT], type[_NodesT2], type[_NodesT3]], skip_klass: SkipKlassT = ..., ) -> Iterator[_NodesT] | Iterator[_NodesT2] | Iterator[_NodesT3]: ... @overload def nodes_of_class( self, klass: tuple[type[_NodesT], ...], skip_klass: SkipKlassT = ..., ) -> Iterator[_NodesT]: ...
[docs] def nodes_of_class( # type: ignore[misc] # mypy doesn't correctly recognize the overloads self, klass: ( type[_NodesT] | tuple[type[_NodesT], type[_NodesT2]] | tuple[type[_NodesT], type[_NodesT2], type[_NodesT3]] | tuple[type[_NodesT], ...] ), skip_klass: SkipKlassT = None, ) -> Iterator[_NodesT] | Iterator[_NodesT2] | Iterator[_NodesT3]: """Get the nodes (including this one or below) of the given types. :param klass: The types of node to search for. :param skip_klass: The types of node to ignore. This is useful to ignore subclasses of :attr:`klass`. :returns: The node of the given types. """ if isinstance(self, klass): yield self if skip_klass is None: for child_node in self.get_children(): yield from child_node.nodes_of_class(klass, skip_klass) return for child_node in self.get_children(): if isinstance(child_node, skip_klass): continue yield from child_node.nodes_of_class(klass, skip_klass)
@cached_property def _assign_nodes_in_scope(self) -> list[nodes.Assign]: return [] def _get_name_nodes(self): for child_node in self.get_children(): yield from child_node._get_name_nodes() def _get_return_nodes_skip_functions(self): yield from () def _get_yield_nodes_skip_functions(self): yield from () def _get_yield_nodes_skip_lambdas(self): yield from () def _infer_name(self, frame, name): # overridden for ImportFrom, Import, Global, Try, TryStar and Arguments pass def _infer( self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]: """We don't know how to resolve a statement by default.""" # this method is overridden by most concrete classes raise InferenceError( "No inference function for {node!r}.", node=self, context=context )
[docs] def inferred(self): """Get a list of the inferred values. .. seealso:: :ref:`inference` :returns: The inferred values. :rtype: list """ return list(self.infer())
[docs] def instantiate_class(self): """Instantiate an instance of the defined class. .. note:: On anything other than a :class:`ClassDef` this will return self. :returns: An instance of the defined class. :rtype: object """ return self
[docs] def has_base(self, node) -> bool: """Check if this node inherits from the given type. :param node: The node defining the base to look for. Usually this is a :class:`Name` node. :type node: NodeNG """ return False
[docs] def callable(self) -> bool: """Whether this node defines something that is callable. :returns: Whether this defines something that is callable. """ return False
[docs] def eq(self, value) -> bool: return False
[docs] def as_string(self) -> str: """Get the source code that this node represents.""" return AsStringVisitor()(self)
[docs] def repr_tree( self, ids=False, include_linenos=False, ast_state=False, indent=" ", max_depth=0, max_width=80, ) -> str: """Get a string representation of the AST from this node. :param ids: If true, includes the ids with the node type names. :type ids: bool :param include_linenos: If true, includes the line numbers and column offsets. :type include_linenos: bool :param ast_state: If true, includes information derived from the whole AST like local and global variables. :type ast_state: bool :param indent: A string to use to indent the output string. :type indent: str :param max_depth: If set to a positive integer, won't return nodes deeper than max_depth in the string. :type max_depth: int :param max_width: Attempt to format the output string to stay within this number of characters, but can exceed it under some circumstances. Only positive integer values are valid, the default is 80. :type max_width: int :returns: The string representation of the AST. :rtype: str """ @_singledispatch def _repr_tree(node, result, done, cur_indent="", depth=1): """Outputs a representation of a non-tuple/list, non-node that's contained within an AST, including strings. """ lines = pprint.pformat( node, width=max(max_width - len(cur_indent), 1) ).splitlines(True) result.append(lines[0]) result.extend([cur_indent + line for line in lines[1:]]) return len(lines) != 1 # pylint: disable=unused-variable,useless-suppression; doesn't understand singledispatch @_repr_tree.register(tuple) @_repr_tree.register(list) def _repr_seq(node, result, done, cur_indent="", depth=1): """Outputs a representation of a sequence that's contained within an AST. """ cur_indent += indent result.append("[") if not node: broken = False elif len(node) == 1: broken = _repr_tree(node[0], result, done, cur_indent, depth) elif len(node) == 2: broken = _repr_tree(node[0], result, done, cur_indent, depth) if not broken: result.append(", ") else: result.append(",\n") result.append(cur_indent) broken = _repr_tree(node[1], result, done, cur_indent, depth) or broken else: result.append("\n") result.append(cur_indent) for child in node[:-1]: _repr_tree(child, result, done, cur_indent, depth) result.append(",\n") result.append(cur_indent) _repr_tree(node[-1], result, done, cur_indent, depth) broken = True result.append("]") return broken # pylint: disable=unused-variable,useless-suppression; doesn't understand singledispatch @_repr_tree.register(NodeNG) def _repr_node(node, result, done, cur_indent="", depth=1): """Outputs a strings representation of an astroid node.""" if node in done: result.append( indent + f"<Recursion on {type(node).__name__} with id={id(node)}" ) return False done.add(node) if max_depth and depth > max_depth: result.append("...") return False depth += 1 cur_indent += indent if ids: result.append(f"{type(node).__name__}<0x{id(node):x}>(\n") else: result.append(f"{type(node).__name__}(") fields = [] if include_linenos: fields.extend(("lineno", "col_offset")) fields.extend(node._other_fields) fields.extend(node._astroid_fields) if ast_state: fields.extend(node._other_other_fields) if not fields: broken = False elif len(fields) == 1: result.append(f"{fields[0]}=") broken = _repr_tree( getattr(node, fields[0]), result, done, cur_indent, depth ) else: result.append("\n") result.append(cur_indent) for field in fields[:-1]: # TODO: Remove this after removal of the 'doc' attribute if field == "doc": continue result.append(f"{field}=") _repr_tree(getattr(node, field), result, done, cur_indent, depth) result.append(",\n") result.append(cur_indent) result.append(f"{fields[-1]}=") _repr_tree(getattr(node, fields[-1]), result, done, cur_indent, depth) broken = True result.append(")") return broken result: list[str] = [] _repr_tree(self, result, set()) return "".join(result)
[docs] def bool_value(self, context: InferenceContext | None = None): """Determine the boolean value of this node. The boolean value of a node can have three possible values: * False: For instance, empty data structures, False, empty strings, instances which return explicitly False from the __nonzero__ / __bool__ method. * True: Most of constructs are True by default: classes, functions, modules etc * Uninferable: The inference engine is uncertain of the node's value. :returns: The boolean value of this node. :rtype: bool or Uninferable """ return util.Uninferable
[docs] def op_precedence(self): # Look up by class name or default to highest precedence return OP_PRECEDENCE.get(self.__class__.__name__, len(OP_PRECEDENCE))
[docs] def op_left_associative(self) -> bool: # Everything is left associative except `**` and IfExp return True