Source code for rattle.ast

from __future__ import annotations

import ast
import platform
from dataclasses import dataclass
from typing import Protocol, runtime_checkable

import libcst as cst
from libcst.metadata import BatchableMetadataProvider, CodePosition, CodeRange


[docs] class AstParseError(SyntaxError): """Raised when the running Python interpreter cannot build a requested AST.""" def __init__(self, syntax_error: SyntaxError) -> None: self.syntax_error = syntax_error self.python_version = platform.python_version() self.message = syntax_error.msg self.line = max(syntax_error.lineno or 1, 1) self.column = max((syntax_error.offset or 1) - 1, 0) end_line = getattr(syntax_error, "end_lineno", None) end_column = getattr(syntax_error, "end_offset", None) self.end_line = end_line if isinstance(end_line, int) else None self.end_column = ( max(end_column - 1, 0) if isinstance(end_column, int) and end_column > 0 else None ) super().__init__( f"Unable to build an AST using Python {self.python_version}: {self.message}" )
@runtime_checkable class _PositionedAstNode(Protocol): lineno: int col_offset: int end_lineno: int | None end_col_offset: int | None
[docs] @dataclass(frozen=True) class AstContext: """A parsed Python AST and source-aware conversion to Rattle code ranges.""" tree: ast.Module _source_lines: tuple[str, ...]
[docs] def code_range(self, node: ast.AST) -> CodeRange: """Return the Rattle source range for a positioned AST node.""" if ( not isinstance(node, _PositionedAstNode) or node.end_lineno is None or node.end_col_offset is None ): raise ValueError(f"{type(node).__name__} does not have a complete source position") start = self._code_position(node.lineno, node.col_offset) end = self._code_position(node.end_lineno, node.end_col_offset) if (end.line, end.column) < (start.line, start.column): raise ValueError(f"{type(node).__name__} has an invalid source position") return CodeRange(start=start, end=end)
def _code_position(self, line: int, byte_column: int) -> CodePosition: if line < 1 or line > len(self._source_lines): raise ValueError(f"AST source line {line} is outside the parsed module") if byte_column < 0: raise ValueError(f"AST source column {byte_column} cannot be negative") source_line = self._source_lines[line - 1] encoded_line = source_line.encode("utf-8") if byte_column > len(encoded_line): raise ValueError(f"AST source column {byte_column} is outside source line {line}") try: column = len(encoded_line[:byte_column].decode("utf-8")) except UnicodeDecodeError as e: raise ValueError( f"AST source column {byte_column} splits a character on source line {line}" ) from e return CodePosition(line=line, column=column)
[docs] class AstProvider(BatchableMetadataProvider[AstContext]): """Provide a cached CPython AST for the current LibCST module."""
[docs] def visit_Module(self, node: cst.Module) -> None: source = node.code try: tree = ast.parse(source, type_comments=True) except SyntaxError as e: raise AstParseError(e) from e source_lines = tuple(source.splitlines(keepends=True)) if not source_lines: source_lines = ("",) self.set_metadata(node, AstContext(tree=tree, _source_lines=source_lines))
__all__ = ["AstContext", "AstParseError", "AstProvider"]