| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045 |
- import ast
- import sys
- import dis
- from types import CodeType, FrameType
- from typing import Any, Callable, Iterator, Optional, Sequence, Set, Tuple, Type, Union, cast
- from .executing import EnhancedAST, NotOneValueFound, Source, only, function_node_types, assert_
- from ._exceptions import KnownIssue, VerifierFailure
- from ._utils import mangled_name
- from functools import lru_cache
- import itertools
- # the code in this module can use all python>=3.11 features
- def parents(node: EnhancedAST) -> Iterator[EnhancedAST]:
- while True:
- if hasattr(node, "parent"):
- node = node.parent
- yield node
- else:
- break # pragma: no mutate
- def node_and_parents(node: EnhancedAST) -> Iterator[EnhancedAST]:
- yield node
- yield from parents(node)
- @lru_cache(128) # pragma: no mutate
- def get_instructions(code: CodeType) -> list[dis.Instruction]:
- return list(dis.get_instructions(code))
- types_cmp_issue_fix = (
- ast.IfExp,
- ast.If,
- ast.Assert,
- ast.While,
- )
- types_cmp_issue = types_cmp_issue_fix + (
- ast.ListComp,
- ast.SetComp,
- ast.DictComp,
- ast.GeneratorExp,
- )
- op_type_map = {
- "**": ast.Pow,
- "*": ast.Mult,
- "@": ast.MatMult,
- "//": ast.FloorDiv,
- "/": ast.Div,
- "%": ast.Mod,
- "+": ast.Add,
- "-": ast.Sub,
- "<<": ast.LShift,
- ">>": ast.RShift,
- "&": ast.BitAnd,
- "^": ast.BitXor,
- "|": ast.BitOr,
- }
- class PositionNodeFinder(object):
- """
- Mapping bytecode to ast-node based on the source positions, which where introduced in pyhon 3.11.
- In general every ast-node can be exactly referenced by its begin/end line/col_offset, which is stored in the bytecode.
- There are only some exceptions for methods and attributes.
- """
- def __init__(self, frame: FrameType, stmts: Set[EnhancedAST], tree: ast.Module, lasti: int, source: Source):
- self.bc_dict={bc.offset:bc for bc in get_instructions(frame.f_code) }
- self.frame=frame
- self.source = source
- self.decorator: Optional[EnhancedAST] = None
- # work around for https://github.com/python/cpython/issues/96970
- while self.opname(lasti) == "CACHE":
- lasti -= 2
- try:
- # try to map with all match_positions
- self.result = self.find_node(lasti)
- except NotOneValueFound:
- typ: tuple[Type]
- # LOAD_METHOD could load "".join for long "..."%(...) BinOps
- # this can only be associated by using all positions
- if self.opname(lasti) in (
- "LOAD_METHOD",
- "LOAD_ATTR",
- "STORE_ATTR",
- "DELETE_ATTR",
- ):
- # lineno and col_offset of LOAD_METHOD and *_ATTR instructions get set to the beginning of
- # the attribute by the python compiler to improved error messages (PEP-657)
- # we ignore here the start position and try to find the ast-node just by end position and expected node type
- # This is save, because there can only be one attribute ending at a specific point in the source code.
- typ = (ast.Attribute,)
- elif self.opname(lasti) in ("CALL", "CALL_KW"):
- # A CALL instruction can be a method call, in which case the lineno and col_offset gets changed by the compiler.
- # Therefore we ignoring here this attributes and searchnig for a Call-node only by end_col_offset and end_lineno.
- # This is save, because there can only be one method ending at a specific point in the source code.
- # One closing ) only belongs to one method.
- typ = (ast.Call,)
- else:
- raise
- self.result = self.find_node(
- lasti,
- match_positions=("end_col_offset", "end_lineno"),
- typ=typ,
- )
- instruction = self.instruction(lasti)
- assert instruction is not None
- self.result = self.fix_result(self.result, instruction)
- self.known_issues(self.result, instruction)
- self.test_for_decorator(self.result, lasti)
- # verify
- if self.decorator is None:
- self.verify(self.result, instruction)
- else:
- assert_(self.decorator in self.result.decorator_list)
- def test_for_decorator(self, node: EnhancedAST, index: int) -> None:
- if (
- isinstance(node.parent, (ast.ClassDef, function_node_types))
- and node in node.parent.decorator_list # type: ignore[attr-defined]
- ):
- node_func = node.parent
- while True:
- # the generated bytecode looks like follow:
- # index opname
- # ------------------
- # index-4 PRECALL (only in 3.11)
- # index-2 CACHE
- # index CALL <- the call instruction
- # ... CACHE some CACHE instructions
- # maybe multiple other bytecode blocks for other decorators
- # index-4 PRECALL (only in 3.11)
- # index-2 CACHE
- # index CALL <- index of the next loop
- # ... CACHE some CACHE instructions
- # index+x STORE_* the ast-node of this instruction points to the decorated thing
- if not (
- (self.opname(index - 4) == "PRECALL" or sys.version_info >= (3, 12))
- and self.opname(index) == "CALL"
- ): # pragma: no mutate
- break # pragma: no mutate
- index += 2
- while self.opname(index) in ("CACHE", "EXTENDED_ARG"):
- index += 2
- if (
- self.opname(index).startswith("STORE_")
- and self.find_node(index) == node_func
- ):
- self.result = node_func
- self.decorator = node
- return
- if sys.version_info < (3, 12):
- index += 4
- def fix_result(
- self, node: EnhancedAST, instruction: dis.Instruction
- ) -> EnhancedAST:
- if (
- sys.version_info >= (3, 12, 5)
- and instruction.opname in ("GET_ITER", "FOR_ITER")
- and isinstance(node.parent, ast.For)
- and node is node.parent.iter
- ):
- # node positions have changed in 3.12.5
- # https://github.com/python/cpython/issues/93691
- # `for` calls __iter__ and __next__ during execution, the calling
- # expression of these calls was the ast.For node since cpython 3.11 (see test_iter).
- # cpython 3.12.5 changed this to the `iter` node of the loop, to make tracebacks easier to read.
- # This keeps backward compatibility with older executing versions.
- # there are also cases like:
- #
- # for a in iter(l): pass
- #
- # where `iter(l)` would be otherwise the resulting node for the `iter()` call and the __iter__ call of the for implementation.
- # keeping the old behaviour makes it possible to distinguish both cases.
- return node.parent
- if (
- sys.version_info >= (3, 12, 6)
- and instruction.opname in ("GET_ITER", "FOR_ITER")
- and isinstance(
- node.parent.parent,
- (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp),
- )
- and isinstance(node.parent,ast.comprehension)
- and node is node.parent.iter
- ):
- # same as above but only for comprehensions, see:
- # https://github.com/python/cpython/issues/123142
- return node.parent.parent
- if sys.version_info >= (3, 12,6) and instruction.opname == "CALL":
- before = self.instruction_before(instruction)
- if (
- before is not None
- and before.opname == "LOAD_CONST"
- and before.positions == instruction.positions
- and isinstance(node.parent, ast.withitem)
- and node is node.parent.context_expr
- ):
- # node positions for with-statements have change
- # and is now equal to the expression which created the context-manager
- # https://github.com/python/cpython/pull/120763
- # with context_manager:
- # ...
- # but there is one problem to distinguish call-expressions from __exit__()
- # with context_manager():
- # ...
- # the call for __exit__
- # 20 1:5 1:22 LOAD_CONST(None)
- # 22 1:5 1:22 LOAD_CONST(None)
- # 24 1:5 1:22 LOAD_CONST(None)
- # 26 1:5 1:22 CALL() # <-- same source range as context_manager()
- # but we can use the fact that the previous load for None
- # has the same source range as the call, wich can not happen for normal calls
- # we return the same ast.With statement at the and to preserve backward compatibility
- return node.parent.parent
- if (
- sys.version_info >= (3, 12,6)
- and instruction.opname == "BEFORE_WITH"
- and isinstance(node.parent, ast.withitem)
- and node is node.parent.context_expr
- ):
- # handle positions changes for __enter__
- return node.parent.parent
- if sys.version_info >= (3, 14) and instruction.opname == "CALL":
- before = self.instruction_before(instruction)
- if (
- before is not None
- and before.opname == "LOAD_SPECIAL"
- and before.argrepr in ("__enter__","__aenter__")
- and before.positions == instruction.positions
- and isinstance(node.parent, ast.withitem)
- and node is node.parent.context_expr
- ):
- return node.parent.parent
- if sys.version_info >= (3, 14) and isinstance(node, ast.UnaryOp) and isinstance(node.op,ast.Not) and instruction.opname !="UNARY_NOT":
- # fix for https://github.com/python/cpython/issues/137843
- return node.operand
- return node
- def known_issues(self, node: EnhancedAST, instruction: dis.Instruction) -> None:
- if instruction.opname in ("COMPARE_OP", "IS_OP", "CONTAINS_OP") and isinstance(
- node, types_cmp_issue
- ):
- if isinstance(node, types_cmp_issue_fix):
- # this is a workaround for https://github.com/python/cpython/issues/95921
- # we can fix cases with only on comparison inside the test condition
- #
- # we can not fix cases like:
- # if a<b<c and d<e<f: pass
- # if (a<b<c)!=d!=e: pass
- # because we don't know which comparison caused the problem
- comparisons = [
- n
- for n in ast.walk(node.test) # type: ignore[attr-defined]
- if isinstance(n, ast.Compare) and len(n.ops) > 1
- ]
- assert_(comparisons, "expected at least one comparison")
- if len(comparisons) == 1:
- node = self.result = cast(EnhancedAST, comparisons[0])
- else:
- raise KnownIssue(
- "multiple chain comparison inside %s can not be fixed" % (node)
- )
- else:
- # Comprehension and generators get not fixed for now.
- raise KnownIssue("chain comparison inside %s can not be fixed" % (node))
- if (
- sys.version_info[:3] == (3, 11, 1)
- and isinstance(node, ast.Compare)
- and instruction.opname == "CALL"
- and any(isinstance(n, ast.Assert) for n in node_and_parents(node))
- ):
- raise KnownIssue(
- "known bug in 3.11.1 https://github.com/python/cpython/issues/95921"
- )
- if isinstance(node, ast.Assert):
- # pytest assigns the position of the assertion to all expressions of the rewritten assertion.
- # All the rewritten expressions get mapped to ast.Assert, which is the wrong ast-node.
- # We don't report this wrong result.
- raise KnownIssue("assert")
- if any(isinstance(n, ast.pattern) for n in node_and_parents(node)):
- # TODO: investigate
- raise KnownIssue("pattern matching ranges seems to be wrong")
- if (
- sys.version_info >= (3, 12)
- and isinstance(node, ast.Call)
- and isinstance(node.func, ast.Name)
- and node.func.id == "super"
- ):
- # super is optimized to some instructions which do not map nicely to a Call
- # find the enclosing function
- func = node.parent
- while hasattr(func, "parent") and not isinstance(
- func, (ast.AsyncFunctionDef, ast.FunctionDef)
- ):
- func = func.parent
- # get the first function argument (self/cls)
- first_arg = None
- if hasattr(func, "args"):
- args = [*func.args.posonlyargs, *func.args.args]
- if args:
- first_arg = args[0].arg
- if (instruction.opname, instruction.argval) in [
- ("LOAD_DEREF", "__class__"),
- ("LOAD_FAST", first_arg),
- ("LOAD_FAST_BORROW", first_arg),
- ("LOAD_DEREF", first_arg),
- ]:
- raise KnownIssue("super optimization")
- if self.is_except_cleanup(instruction, node):
- raise KnownIssue("exeption cleanup does not belong to the last node in a except block")
- if instruction.opname == "STORE_NAME" and instruction.argval == "__classcell__":
- # handle stores to __classcell__ as KnownIssue,
- # because they get complicated if they are used in `if` or `for` loops
- # example:
- #
- # class X:
- # # ... something
- # if some_condition:
- # def method(self):
- # super()
- #
- # The `STORE_NAME` instruction gets mapped to the `ast.If` node,
- # because it is the last element in the class.
- # This last element could be anything and gets dificult to verify.
- raise KnownIssue("store __classcell__")
- if (
- instruction.opname == "CALL"
- and not isinstance(node,ast.Call)
- and any(isinstance(p, ast.Assert) for p in parents(node))
- and sys.version_info >= (3, 11, 2)
- ):
- raise KnownIssue("exception generation maps to condition")
- if sys.version_info >= (3, 13):
- if instruction.opname in (
- "STORE_FAST_STORE_FAST",
- "STORE_FAST_LOAD_FAST",
- "LOAD_FAST_LOAD_FAST",
- ):
- raise KnownIssue(f"can not map {instruction.opname} to two ast nodes")
- if instruction.opname in ("LOAD_FAST","LOAD_FAST_BORROW") and instruction.argval == "__class__":
- # example:
- # class T:
- # def a():
- # super()
- # some_node # <- there is a LOAD_FAST for this node because we use super()
- raise KnownIssue(
- f"loading of __class__ is accociated with a random node at the end of a class if you use super()"
- )
- if (
- instruction.opname == "COMPARE_OP"
- and isinstance(node, ast.UnaryOp)
- and isinstance(node.operand,ast.Compare)
- and isinstance(node.op, ast.Not)
- ):
- # work around for
- # https://github.com/python/cpython/issues/114671
- self.result = node.operand
- if sys.version_info >= (3,14):
- if header_length := self.annotation_header_size():
- last_offset=list(self.bc_dict.keys())[-1]
- if (
- not (header_length*2 < instruction.offset <last_offset-4)
- ):
- # https://github.com/python/cpython/issues/135700
- raise KnownIssue("synthetic opcodes in annotations are just bound to the first node")
- if self.frame.f_code.co_name=="__annotate__" and instruction.opname=="STORE_SUBSCR":
- raise KnownIssue("synthetic code to store annotation")
- if self.frame.f_code.co_name=="__annotate__" and isinstance(node,ast.AnnAssign):
- raise KnownIssue("some opcodes in the annotation are just bound specific nodes")
- if isinstance(node,(ast.TypeAlias)) and self.frame.f_code.co_name==node.name.id :
- raise KnownIssue("some opcodes in the annotation are just bound TypeAlias")
- if instruction.opname == "STORE_NAME" and instruction.argrepr == "__annotate__":
- raise KnownIssue("just a store of the annotation")
- if instruction.opname == "IS_OP" and isinstance(node,ast.Name):
- raise KnownIssue("part of a check that a name like `all` is a builtin")
- def annotation_header_size(self)->int:
- if sys.version_info >=(3,14):
- header=[inst.opname for inst in itertools.islice(self.bc_dict.values(),8)]
- if len(header)==8:
- if header[0] in ("COPY_FREE_VARS","MAKE_CELL"):
- del header[0]
- header_size=8
- else:
- del header[7]
- header_size=7
- if header==[
- "RESUME",
- "LOAD_FAST_BORROW",
- "LOAD_SMALL_INT",
- "COMPARE_OP",
- "POP_JUMP_IF_FALSE",
- "NOT_TAKEN",
- "LOAD_COMMON_CONSTANT",
- ]:
- return header_size
- return 0
- @staticmethod
- def is_except_cleanup(inst: dis.Instruction, node: EnhancedAST) -> bool:
- if inst.opname not in (
- "STORE_NAME",
- "STORE_FAST",
- "STORE_DEREF",
- "STORE_GLOBAL",
- "DELETE_NAME",
- "DELETE_FAST",
- "DELETE_DEREF",
- "DELETE_GLOBAL",
- ):
- return False
- # This bytecode does something exception cleanup related.
- # The position of the instruciton seems to be something in the last ast-node of the ExceptHandler
- # this could be a bug, but it might not be observable in normal python code.
- # example:
- # except Exception as exc:
- # enum_member._value_ = value
- # other example:
- # STORE_FAST of e was mapped to Constant(value=False)
- # except OSError as e:
- # if not _ignore_error(e):
- # raise
- # return False
- # STORE_FAST of msg was mapped to print(...)
- # except TypeError as msg:
- # print("Sorry:", msg, file=file)
- if (
- isinstance(node, ast.Name)
- and isinstance(node.ctx,ast.Store)
- and inst.opname.startswith("STORE_")
- and mangled_name(node) == inst.argval
- ):
- # Storing the variable is valid and no exception cleanup, if the name is correct
- return False
- if (
- isinstance(node, ast.Name)
- and isinstance(node.ctx,ast.Del)
- and inst.opname.startswith("DELETE_")
- and mangled_name(node) == inst.argval
- ):
- # Deleting the variable is valid and no exception cleanup, if the name is correct
- return False
- return any(
- isinstance(n, ast.ExceptHandler) and n.name and mangled_name(n) == inst.argval
- for n in parents(node)
- )
- def verify(self, node: EnhancedAST, instruction: dis.Instruction) -> None:
- """
- checks if this node could gererate this instruction
- """
- op_name = instruction.opname
- extra_filter: Callable[[EnhancedAST], bool] = lambda e: True
- ctx: Type = type(None)
- def inst_match(opnames: Union[str, Sequence[str]], **kwargs: Any) -> bool:
- """
- match instruction
- Parameters:
- opnames: (str|Seq[str]): inst.opname has to be equal to or in `opname`
- **kwargs: every arg has to match inst.arg
- Returns:
- True if all conditions match the instruction
- """
- if isinstance(opnames, str):
- opnames = [opnames]
- return instruction.opname in opnames and kwargs == {
- k: getattr(instruction, k) for k in kwargs
- }
- def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
- """
- match the ast-node
- Parameters:
- node_type: type of the node
- **kwargs: every `arg` has to be equal `node.arg`
- or `node.arg` has to be an instance of `arg` if it is a type.
- """
- return isinstance(node, node_type) and all(
- isinstance(getattr(node, k), v)
- if isinstance(v, type)
- else getattr(node, k) == v
- for k, v in kwargs.items()
- )
- if op_name == "CACHE":
- return
- if inst_match("CALL") and node_match((ast.With, ast.AsyncWith)):
- # call to context.__exit__
- return
- if inst_match(("CALL", "LOAD_FAST","LOAD_FAST_BORROW")) and node_match(
- (ast.ListComp, ast.GeneratorExp, ast.SetComp, ast.DictComp)
- ):
- # call to the generator function
- return
- if (
- sys.version_info >= (3, 12)
- and inst_match(("LOAD_FAST_AND_CLEAR", "STORE_FAST"))
- and node_match((ast.ListComp, ast.SetComp, ast.DictComp))
- ):
- return
- if inst_match(("CALL", "CALL_FUNCTION_EX")) and node_match(
- (ast.ClassDef, ast.Call)
- ):
- return
- if inst_match(("COMPARE_OP", "IS_OP", "CONTAINS_OP")) and node_match(
- ast.Compare
- ):
- return
- if inst_match("LOAD_NAME", argval="__annotations__") and node_match(
- ast.AnnAssign
- ):
- return
- if (
- (
- inst_match("LOAD_METHOD", argval="join")
- or inst_match("LOAD_ATTR", argval="join") # 3.12
- or inst_match(("CALL", "BUILD_STRING"))
- )
- and node_match(ast.BinOp, left=ast.Constant, op=ast.Mod)
- and isinstance(cast(ast.Constant, cast(ast.BinOp, node).left).value, str)
- ):
- # "..."%(...) uses "".join
- return
- if inst_match("STORE_SUBSCR") and node_match(ast.AnnAssign):
- # data: int
- return
- if inst_match(("DELETE_NAME", "DELETE_FAST")) and node_match(
- ast.Name, id=instruction.argval, ctx=ast.Del
- ):
- return
- if inst_match("BUILD_STRING") and (
- node_match(ast.JoinedStr) or node_match(ast.BinOp, op=ast.Mod)
- ):
- return
- if inst_match(("BEFORE_WITH","WITH_EXCEPT_START")) and node_match(ast.With):
- return
- if inst_match(("STORE_NAME", "STORE_GLOBAL"), argval="__doc__") and node_match(
- ast.Constant
- ):
- # store docstrings
- return
- if (
- inst_match(("STORE_NAME", "STORE_FAST", "STORE_GLOBAL", "STORE_DEREF"))
- and node_match(ast.ExceptHandler)
- and instruction.argval == mangled_name(node)
- ):
- # store exception in variable
- return
- if (
- inst_match(("STORE_NAME", "STORE_FAST", "STORE_DEREF", "STORE_GLOBAL"))
- and node_match((ast.Import, ast.ImportFrom))
- and any(mangled_name(cast(EnhancedAST, alias)) == instruction.argval for alias in cast(ast.Import, node).names)
- ):
- # store imported module in variable
- return
- if (
- inst_match(("STORE_FAST", "STORE_DEREF", "STORE_NAME", "STORE_GLOBAL"))
- and (
- node_match((ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef))
- or node_match(
- ast.Name,
- ctx=ast.Store,
- )
- )
- and instruction.argval == mangled_name(node)
- ):
- return
- if False:
- # TODO: match expressions are not supported for now
- if inst_match(("STORE_FAST", "STORE_NAME")) and node_match(
- ast.MatchAs, name=instruction.argval
- ):
- return
- if inst_match("COMPARE_OP", argval="==") and node_match(ast.MatchSequence):
- return
- if inst_match("COMPARE_OP", argval="==") and node_match(ast.MatchValue):
- return
- if inst_match("BINARY_OP"):
- arg=instruction.argrepr.removesuffix("=")
- if arg!="[]" and node_match( ast.AugAssign, op=op_type_map[arg]):
- # a+=5
- return
- if node_match(ast.Attribute, ctx=ast.Del) and inst_match(
- "DELETE_ATTR", argval=mangled_name(node)
- ):
- return
- if inst_match(
- (
- "JUMP_IF_TRUE_OR_POP",
- "JUMP_IF_FALSE_OR_POP",
- "POP_JUMP_IF_TRUE",
- "POP_JUMP_IF_FALSE",
- )
- ) and node_match(ast.BoolOp):
- # and/or short circuit
- return
- if inst_match("DELETE_SUBSCR") and node_match(ast.Subscript, ctx=ast.Del):
- return
- if (
- node_match(ast.Name, ctx=ast.Load)
- or (
- node_match(ast.Name, ctx=ast.Store)
- and isinstance(node.parent, ast.AugAssign)
- )
- ) and inst_match(
- (
- "LOAD_NAME",
- "LOAD_FAST",
- "LOAD_FAST_CHECK",
- "LOAD_FAST_BORROW",
- "LOAD_GLOBAL",
- "LOAD_DEREF",
- "LOAD_FROM_DICT_OR_DEREF",
- "LOAD_FAST_BORROW_LOAD_FAST_BORROW",
- ),
- ) and (
- mangled_name(node) in instruction.argval if isinstance(instruction.argval,tuple)
- else instruction.argval == mangled_name(node)
- ):
- return
- if node_match(ast.Name, ctx=ast.Del) and inst_match(
- ("DELETE_NAME", "DELETE_GLOBAL", "DELETE_DEREF"), argval=mangled_name(node)
- ):
- return
- if node_match(ast.Constant) and inst_match(
- ("LOAD_CONST","LOAD_SMALL_INT"), argval=cast(ast.Constant, node).value
- ):
- return
- if node_match(
- (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp, ast.For)
- ) and inst_match(("GET_ITER", "FOR_ITER")):
- return
- if sys.version_info >= (3, 12):
- if node_match(ast.UnaryOp, op=ast.UAdd) and inst_match(
- "CALL_INTRINSIC_1", argrepr="INTRINSIC_UNARY_POSITIVE"
- ):
- return
- if node_match(ast.Subscript) and inst_match("BINARY_SLICE"):
- return
- if node_match(ast.ImportFrom) and inst_match(
- "CALL_INTRINSIC_1", argrepr="INTRINSIC_IMPORT_STAR"
- ):
- return
- if (
- node_match(ast.Yield) or isinstance(node.parent, ast.GeneratorExp)
- ) and inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_ASYNC_GEN_WRAP"):
- return
- if node_match(ast.Name) and inst_match("LOAD_DEREF",argval="__classdict__"):
- return
- if node_match(ast.TypeVar) and (
- inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_TYPEVAR")
- or inst_match(
- "CALL_INTRINSIC_2", argrepr="INTRINSIC_TYPEVAR_WITH_BOUND"
- )
- or inst_match(
- "CALL_INTRINSIC_2", argrepr="INTRINSIC_TYPEVAR_WITH_CONSTRAINTS"
- )
- or inst_match(("STORE_FAST", "STORE_DEREF"), argrepr=mangled_name(node))
- ):
- return
- if node_match(ast.TypeVarTuple) and (
- inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_TYPEVARTUPLE")
- or inst_match(("STORE_FAST", "STORE_DEREF"), argrepr=node.name)
- ):
- return
- if node_match(ast.ParamSpec) and (
- inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_PARAMSPEC")
- or inst_match(("STORE_FAST", "STORE_DEREF"), argrepr=node.name)):
- return
- if node_match(ast.TypeAlias):
- if(
- inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_TYPEALIAS")
- or inst_match(
- ("STORE_NAME", "STORE_FAST", "STORE_DEREF","STORE_GLOBAL"), argrepr=node.name.id
- )
- or inst_match("CALL")
- ):
- return
- if node_match(ast.ClassDef) and node.type_params:
- if inst_match(
- ("STORE_DEREF", "LOAD_DEREF", "LOAD_FROM_DICT_OR_DEREF"),
- argrepr=".type_params",
- ):
- return
- if inst_match(("STORE_FAST", "LOAD_FAST"), argrepr=".generic_base"):
- return
- if inst_match(
- "CALL_INTRINSIC_1", argrepr="INTRINSIC_SUBSCRIPT_GENERIC"
- ):
- return
- if inst_match("LOAD_DEREF",argval="__classdict__"):
- return
- if node_match((ast.FunctionDef,ast.AsyncFunctionDef)) and node.type_params:
- if inst_match("CALL"):
- return
- if inst_match(
- "CALL_INTRINSIC_2", argrepr="INTRINSIC_SET_FUNCTION_TYPE_PARAMS"
- ):
- return
- if inst_match("LOAD_FAST",argval=".defaults"):
- return
- if inst_match("LOAD_FAST",argval=".kwdefaults"):
- return
- if sys.version_info >= (3, 14):
- if inst_match("LOAD_FAST_BORROW_LOAD_FAST_BORROW",argval=(".defaults",".kwdefaults")):
- return
- if inst_match("STORE_NAME", argval="__classdictcell__"):
- # this is a general thing
- return
- # f-strings
- if node_match(ast.JoinedStr) and (
- inst_match("LOAD_ATTR", argval="join")
- or inst_match(("LIST_APPEND", "CALL"))
- ):
- return
- if node_match(ast.FormattedValue) and inst_match("FORMAT_VALUE"):
- return
- if sys.version_info >= (3, 13):
- if inst_match("NOP"):
- return
- if inst_match("TO_BOOL") and node_match(ast.BoolOp):
- return
- if inst_match("CALL_KW") and node_match((ast.Call, ast.ClassDef)):
- return
- if inst_match("LOAD_FAST", argval=".type_params"):
- return
- if inst_match("LOAD_FAST", argval="__classdict__"):
- return
- if inst_match(("LOAD_FAST","LOAD_FAST_BORROW")) and node_match(
- (
- ast.FunctionDef,
- ast.ClassDef,
- ast.TypeAlias,
- ast.TypeVar,
- ast.Lambda,
- ast.AsyncFunctionDef,
- )
- ):
- # These are loads for closure variables.
- # It is difficult to check that this is actually closure variable, see:
- # https://github.com/alexmojaki/executing/pull/80#discussion_r1716027317
- return
- if (
- inst_match("LOAD_FAST")
- and node_match(ast.TypeAlias)
- and node.name.id == instruction.argval
- ):
- return
- if inst_match("STORE_NAME",argval="__static_attributes__"):
- # the node is the first node in the body
- return
- if inst_match(("LOAD_FAST","LOAD_FAST_BORROW")) and isinstance(node.parent,ast.TypeVar):
- return
- if inst_match("CALL_INTRINSIC_2",argrepr="INTRINSIC_SET_TYPEPARAM_DEFAULT") and node_match((ast.TypeVar,ast.ParamSpec,ast.TypeVarTuple)):
- return
- if sys.version_info >= (3, 14):
- if inst_match("BINARY_OP",argrepr="[]") and node_match(ast.Subscript):
- return
- if inst_match("LOAD_FAST_BORROW", argval="__classdict__"):
- return
- if inst_match(("STORE_NAME","LOAD_NAME"), argval="__conditional_annotations__"):
- return
- if inst_match("LOAD_FAST_BORROW_LOAD_FAST_BORROW") and node_match(ast.Name) and node.id in instruction.argval:
- return
- # old verifier
- typ: Type = type(None)
- op_type: Type = type(None)
- if op_name.startswith(("BINARY_SUBSCR", "SLICE+")):
- typ = ast.Subscript
- ctx = ast.Load
- elif op_name.startswith("BINARY_"):
- typ = ast.BinOp
- op_type = op_type_map[instruction.argrepr]
- extra_filter = lambda e: isinstance(cast(ast.BinOp, e).op, op_type)
- elif op_name.startswith("UNARY_"):
- typ = ast.UnaryOp
- op_type = dict(
- UNARY_POSITIVE=ast.UAdd,
- UNARY_NEGATIVE=ast.USub,
- UNARY_NOT=ast.Not,
- UNARY_INVERT=ast.Invert,
- )[op_name]
- extra_filter = lambda e: isinstance(cast(ast.UnaryOp, e).op, op_type)
- elif op_name in ("LOAD_ATTR", "LOAD_METHOD", "LOOKUP_METHOD","LOAD_SUPER_ATTR"):
- typ = ast.Attribute
- ctx = ast.Load
- extra_filter = lambda e: mangled_name(e) == instruction.argval
- elif op_name in (
- "LOAD_NAME",
- "LOAD_GLOBAL",
- "LOAD_FAST",
- "LOAD_DEREF",
- "LOAD_CLASSDEREF",
- ):
- typ = ast.Name
- ctx = ast.Load
- extra_filter = lambda e: cast(ast.Name, e).id == instruction.argval
- elif op_name in ("COMPARE_OP", "IS_OP", "CONTAINS_OP"):
- typ = ast.Compare
- extra_filter = lambda e: len(cast(ast.Compare, e).ops) == 1
- elif op_name.startswith(("STORE_SLICE", "STORE_SUBSCR")):
- ctx = ast.Store
- typ = ast.Subscript
- elif op_name.startswith("STORE_ATTR"):
- ctx = ast.Store
- typ = ast.Attribute
- extra_filter = lambda e: mangled_name(e) == instruction.argval
- node_ctx = getattr(node, "ctx", None)
- ctx_match = (
- ctx is not type(None)
- or not hasattr(node, "ctx")
- or isinstance(node_ctx, ctx)
- )
- # check for old verifier
- if isinstance(node, typ) and ctx_match and extra_filter(node):
- return
- # generate error
- title = "ast.%s is not created from %s" % (
- type(node).__name__,
- instruction.opname,
- )
- raise VerifierFailure(title, node, instruction)
- def instruction(self, index: int) -> Optional[dis.Instruction]:
- return self.bc_dict.get(index,None)
- def instruction_before(
- self, instruction: dis.Instruction
- ) -> Optional[dis.Instruction]:
- return self.bc_dict.get(instruction.offset - 2, None)
- def opname(self, index: int) -> str:
- i=self.instruction(index)
- if i is None:
- return "CACHE"
- return i.opname
- extra_node_types=()
- if sys.version_info >= (3,12):
- extra_node_types = (ast.type_param,)
- def find_node(
- self,
- index: int,
- match_positions: Sequence[str] = (
- "lineno",
- "end_lineno",
- "col_offset",
- "end_col_offset",
- ),
- typ: tuple[Type, ...] = (
- ast.expr,
- ast.stmt,
- ast.excepthandler,
- ast.pattern,
- *extra_node_types,
- ),
- ) -> EnhancedAST:
- instruction = self.instruction(index)
- assert instruction is not None
- position = instruction.positions
- assert position is not None and position.lineno is not None
- return only(
- cast(EnhancedAST, node)
- for node in self.source._nodes_by_line[position.lineno]
- if isinstance(node, typ)
- if not isinstance(node, ast.Expr)
- # matchvalue.value has the same positions as matchvalue themself, so we exclude ast.MatchValue
- if not isinstance(node, ast.MatchValue)
- if all(
- getattr(position, attr) == getattr(node, attr)
- for attr in match_positions
- )
- )
|