1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
import ast
import copy
import dis
import sys
sentinel_rep = 2
# generate sentinel at runtime to keep it out of the bytecode
# this allows the algorithm to check also this file
sentinel = "xsglegahghegflgfaih" * sentinel_rep
def constant(value):
if sys.version_info >= (3, 6):
return ast.Constant(value=value)
elif isinstance(value, int):
return ast.Num(value)
elif isinstance(value, str):
return ast.Str(value)
else:
raise TypeError
def index(value):
if sys.version_info >= (3, 9):
return constant(value)
else:
return ast.Index(constant(value))
class DeadcodeTransformer(ast.NodeTransformer):
def visit(self, node):
constant_type = (
ast.Constant
if sys.version_info >= (3, 6)
else (ast.Num, ast.Str, ast.Bytes, ast.NameConstant, ast.Ellipsis)
)
if getattr(node, "_check_is_deadcode", False):
if isinstance(node, constant_type) and isinstance(node.value, str):
# docstring for example
return constant(sentinel)
elif isinstance(node, ast.stmt):
return ast.Expr(
value=ast.Call(
func=ast.Name(id="foo", ctx=ast.Load()),
args=[constant(sentinel)],
keywords=[],
)
)
elif isinstance(node, ast.expr):
if hasattr(node, "ctx") and isinstance(node.ctx, (ast.Store, ast.Del)):
return ast.Subscript(
value=ast.Name(id="foo", ctx=ast.Load()),
slice=index(sentinel),
ctx=node.ctx,
)
else:
return ast.Subscript(
value=ast.Tuple(
elts=[node, constant(sentinel)],
ctx=ast.Load(),
),
slice=index(0),
ctx=ast.Load(),
)
else:
raise TypeError(node)
else:
return super().visit(node)
def is_deadcode(node):
if isinstance(node, ast.withitem):
node = node.context_expr
if isinstance(node, ast.ExceptHandler):
node = node.body[0]
if sys.version_info >= (3,8) and isinstance(node.parent, ast.NamedExpr) and node.parent.target is node:
node = node.parent
if sys.version_info >= (3,12) and isinstance(node.parent,ast.TypeAlias):
node = node.parent
if (
sys.version_info >= (3, 6)
and isinstance(node.parent, ast.AnnAssign)
and node.parent.target is node
):
# AnnAssign.target has to be ast.Name
node = node.parent
if hasattr(node, "_is_deadcode"):
return node._is_deadcode
node._check_is_deadcode = True
module = node
while hasattr(module, "parent"):
module = module.parent
assert isinstance(module, ast.Module)
# create working copy of the ast
module2 = copy.deepcopy(module)
del node._check_is_deadcode
module2 = ast.fix_missing_locations(DeadcodeTransformer().visit(module2))
try:
code = compile(module2, "<filename>", "exec")
except:
print(ast.dump(module2))
raise
visited = set()
def contains_sentinel(code):
if code in visited:
return False
for inst in dis.get_instructions(code):
arg = inst.argval
if isinstance(arg, type(code)) and contains_sentinel(arg):
return True
if arg == sentinel:
return True
visited.add(code)
return False
node._is_deadcode = not contains_sentinel(code)
return node._is_deadcode
|