1import ast
2import os
3import sys
4import tokenize
5
6
7class VultureInputException(Exception):
8    pass
9
10
11def _safe_eval(node, default):
12    """
13    Safely evaluate the Boolean expression under the given AST node.
14
15    Substitute `default` for all sub-expressions that cannot be
16    evaluated (because variables or functions are undefined).
17
18    We could use eval() to evaluate more sub-expressions. However, this
19    function is not safe for arbitrary Python code. Even after
20    overwriting the "__builtins__" dictionary, the original dictionary
21    can be restored
22    (https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html).
23
24    """
25    if isinstance(node, ast.BoolOp):
26        results = [_safe_eval(value, default) for value in node.values]
27        if isinstance(node.op, ast.And):
28            return all(results)
29        else:
30            return any(results)
31    elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
32        return not _safe_eval(node.operand, not default)
33    else:
34        try:
35            return ast.literal_eval(node)
36        except ValueError:
37            return default
38
39
40def condition_is_always_false(condition):
41    return not _safe_eval(condition, True)
42
43
44def condition_is_always_true(condition):
45    return _safe_eval(condition, False)
46
47
48def format_path(path):
49    try:
50        return path.relative_to(os.curdir)
51    except ValueError:
52        # Path is not below the current directory.
53        return path
54
55
56def get_decorator_name(decorator):
57    if isinstance(decorator, ast.Call):
58        decorator = decorator.func
59    parts = []
60    while isinstance(decorator, ast.Attribute):
61        parts.append(decorator.attr)
62        decorator = decorator.value
63    parts.append(decorator.id)
64    return "@" + ".".join(reversed(parts))
65
66
67def get_modules(paths):
68    """Retrieve Python files to check.
69
70    Loop over all given paths, abort if any ends with .pyc and add collect
71    the other given files (even those not ending with .py) and all .py
72    files under the given directories.
73
74    """
75    modules = []
76    for path in paths:
77        path = path.resolve()
78        if path.is_file():
79            if path.suffix == ".pyc":
80                sys.exit(f"Error: *.pyc files are not supported: {path}")
81            else:
82                modules.append(path)
83        elif path.is_dir():
84            modules.extend(path.rglob("*.py"))
85        else:
86            sys.exit(f"Error: {path} could not be found.")
87    return modules
88
89
90def read_file(filename):
91    try:
92        # Use encoding detected by tokenize.detect_encoding().
93        with tokenize.open(filename) as f:
94            return f.read()
95    except (SyntaxError, UnicodeDecodeError) as err:
96        raise VultureInputException(err)
97
98
99class LoggingList(list):
100    def __init__(self, typ, verbose):
101        self.typ = typ
102        self._verbose = verbose
103        return list.__init__(self)
104
105    def append(self, item):
106        if self._verbose:
107            print(f'define {self.typ} "{item.name}"')
108        list.append(self, item)
109
110
111class LoggingSet(set):
112    def __init__(self, typ, verbose):
113        self.typ = typ
114        self._verbose = verbose
115        return set.__init__(self)
116
117    def add(self, name):
118        if self._verbose:
119            print(f'use {self.typ} "{name}"')
120        set.add(self, name)
121