• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyta-uoft / pyta / 18013112007

25 Sep 2025 03:15PM UTC coverage: 94.757% (-0.2%) from 94.944%
18013112007

Pull #1237

github

web-flow
Merge e530258ae into e586102d0
Pull Request #1237: refactoring node_printers.py

44 of 44 new or added lines in 1 file covered. (100.0%)

14 existing lines in 2 files now uncovered.

3524 of 3719 relevant lines covered (94.76%)

17.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.51
/python_ta/checkers/infinite_loop_checker.py
1
"""Check for infinite while loops."""
2

3
import itertools
20✔
4
from typing import Optional
20✔
5

6
from astroid import BoundMethod, InferenceError, UnboundMethod, bases, nodes, util
20✔
7
from pylint.checkers import BaseChecker, utils
20✔
8
from pylint.interfaces import INFERENCE
20✔
9
from pylint.lint import PyLinter
20✔
10

11
IMMUTABLE_TYPES = (
20✔
12
    int,
13
    float,
14
    bool,
15
    complex,
16
    str,
17
    bytes,
18
    tuple,
19
    type(None),
20
)
21

22
CONST_NODES = (
20✔
23
    nodes.Module,
24
    nodes.GeneratorExp,
25
    nodes.Lambda,
26
    nodes.FunctionDef,
27
    nodes.ClassDef,
28
    bases.Generator,
29
    UnboundMethod,
30
    BoundMethod,
31
)
32

33

34
class InfiniteLoopChecker(BaseChecker):
20✔
35
    name = "infinite-loop"
20✔
36
    msgs = {
20✔
37
        "W9501": (
38
            "Infinite while loop: loop does not terminate",
39
            "infinite-loop",
40
            """Used when a while loop is guaranteed to never terminate based on its current structure. This usually
41
            indicates a logical error leading to an unintended infinite loop.""",
42
        ),
43
    }
44

45
    def visit_while(self, node: nodes.While) -> None:
20✔
46
        checks = [
20✔
47
            self._check_condition_constant,
48
            self._check_condition_all_var_used,
49
            self._check_immutable_cond_var_reassigned,
50
        ]
51
        any(check(node) for check in checks)
20✔
52

53
    def _check_condition_all_var_used(self, node: nodes.While) -> bool:
20✔
54
        """Helper function that checks whether variables used in a while loop's condition
55
        are also used anywhere inside the loop body.
56

57
        Note: This is a basic check. It only flags loops where NONE of the condition variables
58
        appear in the body, which indicates an infinite loop.
59
        """
60
        # Get variable(s) used inside condition
61
        cond_vars = set()
20✔
62
        for child in node.test.nodes_of_class(nodes.Name):
20✔
63
            if not isinstance(child.parent, nodes.Call) or child.parent.func is not child:
20✔
64
                cond_vars.add(child.name)
20✔
65
        if not cond_vars:
20✔
66
            return False
20✔
67
        inferred_test = infer_condition(node)
20✔
68
        if not inferred_test:
20✔
69
            return False
20✔
70
        # Check to see if condition variable(s) used inside body
71
        for child in node.body:
20✔
72
            for name_node in child.nodes_of_class((nodes.Name, nodes.AssignName)):
20✔
73
                if name_node.name in cond_vars:
20✔
74
                    # At least one condition variable is used in the loop body
75
                    return False
20✔
76
        else:
77
            self.add_message("infinite-loop", node=node.test, confidence=INFERENCE)
20✔
78
            return True
20✔
79

80
    def _check_condition_constant(self, node: nodes.While) -> bool:
20✔
81
        """Helper function that checks if a constant while-loop condition may lead to an infinite loop.
82

83
        This helper flags loops that meet **both** of the following criteria:
84
            - The `while` condition is constant (e.g., `while 1 < 2`)
85
            - The loop body contains no `return`, `break`, `raise`, `yield`, or `sys.exit()` calls
86
        """
87
        if not self._check_constant_loop_cond(
20✔
88
            node.test
89
        ) and not self._check_constant_form_condition(node):
90
            return False
20✔
91

92
        inferred = infer_condition(node)
20✔
93
        if not inferred:
20✔
94
            return False
20✔
95

96
        check_nodes = (nodes.Break, nodes.Return, nodes.Raise, nodes.Yield)
20✔
97
        for child in node.body:
20✔
98
            for exit_node in child.nodes_of_class(klass=(nodes.Call, *check_nodes)):
20✔
99
                if isinstance(exit_node, check_nodes):
20✔
100
                    return False
20✔
101
                # Check Call node to see if `sys.exit()` is called
102
                elif (
20✔
103
                    isinstance(exit_node, nodes.Call)
104
                    and isinstance(exit_node.func, nodes.Attribute)
105
                    and exit_node.func.attrname == "exit"
106
                ):
107
                    inferred = get_safely_inferred(exit_node.func.expr)
20✔
108
                    if (
20✔
109
                        inferred is not None
110
                        and isinstance(inferred, nodes.Module)
111
                        and inferred.name == "sys"
112
                    ):
113
                        return False
20✔
114
        else:
115
            self.add_message("infinite-loop", node=node.test, confidence=INFERENCE)
20✔
116
            return True
20✔
117

118
    def _check_constant_form_condition(self, node: nodes.While) -> bool:
20✔
119
        """Helper function that checks if while loop condition is of constant form (e.g.: `1 < 2`, `5 - 1 >= 2 + 2`)"""
120
        return not any(node.test.nodes_of_class(nodes.Name))
20✔
121

122
    def _check_constant_loop_cond(self, test_node: Optional[nodes.NodeNG]) -> bool:
20✔
123
        """Helper function that checks if while loop condition is constant.
124

125
        See `https://github.com/pylint-dev/pylint/blob/main/pylint/checkers/base/basic_checker.py#L303` for further
126
        detail."""
127
        structs = (nodes.Dict, nodes.Tuple, nodes.Set, nodes.List)
20✔
128
        except_nodes = (
20✔
129
            nodes.Call,
130
            nodes.BinOp,
131
            nodes.BoolOp,
132
            nodes.UnaryOp,
133
            nodes.Subscript,
134
        )
135
        inferred = None
20✔
136
        maybe_generator_call = None
20✔
137
        emit = isinstance(test_node, (nodes.Const, *structs, *CONST_NODES))
20✔
138
        if not isinstance(test_node, except_nodes):
20✔
139
            inferred = utils.safe_infer(test_node)
20✔
140
            # If we can't infer what the value is but the test is just a variable name
141
            if isinstance(inferred, util.UninferableBase) and isinstance(test_node, nodes.Name):
20✔
142
                emit, maybe_generator_call = InfiniteLoopChecker._name_holds_generator(test_node)
20✔
143

144
        # Emit if calling a function that only returns GeneratorExp (always tests True)
145
        elif isinstance(test_node, nodes.Call):
20✔
146
            maybe_generator_call = test_node
20✔
147

148
        if maybe_generator_call:
20✔
149
            inferred_call = utils.safe_infer(maybe_generator_call.func)
20✔
150
            if isinstance(inferred_call, nodes.FunctionDef):
20✔
151
                # Can't use all(x) or not any(not x) for this condition, because it
152
                # will return True for empty generators, which is not what we want.
UNCOV
153
                all_returns_were_generator = None
×
UNCOV
154
                for return_node in inferred_call._get_return_nodes_skip_functions():
×
UNCOV
155
                    if not isinstance(return_node.value, nodes.GeneratorExp):
×
UNCOV
156
                        all_returns_were_generator = False
×
UNCOV
157
                        break
×
UNCOV
158
                    all_returns_were_generator = True
×
UNCOV
159
                if all_returns_were_generator:
×
UNCOV
160
                    return True
×
161
        if emit:
20✔
162
            return True
20✔
163
        elif isinstance(inferred, CONST_NODES):
20✔
164
            return True
20✔
165
        return False
20✔
166

167
    @staticmethod
20✔
168
    def _name_holds_generator(test_node: nodes.Name) -> tuple[bool, Optional[nodes.Call]]:
20✔
169
        """Return whether `test` tests a name certain to hold a generator, or optionally
170
        a call that should be then tested to see if *it* returns only generators.
171

172
        See `https://github.com/pylint-dev/pylint/blob/main/pylint/checkers/base/basic_checker.py#L303` for further
173
        detail."""
174
        assert isinstance(test_node, nodes.Name)
20✔
175
        emit = False
20✔
176
        maybe_generator_call = None
20✔
177
        lookup_result = test_node.frame().lookup(test_node.name)
20✔
178
        if not lookup_result:
20✔
179
            return emit, maybe_generator_call
×
180
        maybe_generator_assigned = (
20✔
181
            isinstance(assign_name.parent.value, nodes.GeneratorExp)
182
            for assign_name in lookup_result[1]
183
            if isinstance(assign_name.parent, nodes.Assign)
184
        )
185
        first_item = next(maybe_generator_assigned, None)
20✔
186
        if first_item is not None:
20✔
187
            # Emit if this variable is certain to hold a generator
UNCOV
188
            if all(itertools.chain((first_item,), maybe_generator_assigned)):
×
UNCOV
189
                emit = True
×
190
            # If this variable holds the result of a call, save it for next test
UNCOV
191
            elif (
×
192
                len(lookup_result[1]) == 1
193
                and isinstance(lookup_result[1][0].parent, nodes.Assign)
194
                and isinstance(lookup_result[1][0].parent.value, nodes.Call)
195
            ):
UNCOV
196
                maybe_generator_call = lookup_result[1][0].parent.value
×
197
        return emit, maybe_generator_call
20✔
198

199
    def _check_immutable_cond_var_reassigned(self, node: nodes.While) -> bool:
20✔
200
        """Helper function that checks if a while-loop condition uses only immutable variables
201
        and none of them are reassigned inside the loop body.
202

203
        Flags loops that meet **both** of the following criteria:
204
        - All variables in the `while` condition are immutable (int, float, complex, bool,
205
          str, bytes, tuple, or NoneType)
206
        - None of these variables are reassigned in the loop body"""
207
        immutable_vars = set()
20✔
208
        for child in node.test.nodes_of_class(nodes.Name):
20✔
209
            if isinstance(child.parent, nodes.Call) and child.parent.func is child:
20✔
210
                continue
20✔
211
            try:
20✔
212
                inferred_values = list(child.infer())
20✔
213
            except InferenceError:
20✔
214
                return False
20✔
215
            for inferred in inferred_values:
20✔
216
                if inferred is util.Uninferable or not _is_immutable_node(inferred):
20✔
217
                    # Return False when the node may evaluate to a mutable object
218
                    return False
20✔
219
            immutable_vars.add(child.name)
20✔
220
        if not immutable_vars:
20✔
221
            return False
20✔
222
        inferred_test = infer_condition(node)
20✔
223
        if not inferred_test:
20✔
224
            return False
20✔
225

226
        for child in node.body:
20✔
227
            for assign_node in child.nodes_of_class(nodes.AssignName):
20✔
228
                if assign_node.name in immutable_vars:
20✔
229
                    return False
20✔
230
        else:
231
            self.add_message(
20✔
232
                "infinite-loop",
233
                node=node.test,
234
                confidence=INFERENCE,
235
            )
236
            return True
20✔
237

238

239
def _is_immutable_node(node: nodes.NodeNG) -> bool:
20✔
240
    """Helper used to check whether node represents an immutable type."""
241
    return (isinstance(node, nodes.Const) and type(node.value) in IMMUTABLE_TYPES) or isinstance(
20✔
242
        node, nodes.Tuple
243
    )
244

245

246
def get_safely_inferred(node: nodes.NodeNG) -> Optional[nodes.NodeNG]:
20✔
247
    """Helper used to safely infer a node with `astroid.safe_infer`. Return None if inference failed."""
248
    inferred = utils.safe_infer(node)
20✔
249
    if isinstance(inferred, util.UninferableBase) or inferred is None:
20✔
250
        return None
20✔
251
    else:
252
        return inferred
20✔
253

254

255
def infer_condition(node: nodes.While) -> bool:
20✔
256
    """Helper used to safely infer the value of a loop condition. Return False if inference failed or condition
257
    evaluated to be false."""
258
    try:
20✔
259
        inferred_values = list(node.test.infer())
20✔
260
    except InferenceError:
20✔
261
        return True
20✔
262
    for inferred in inferred_values:
20✔
263
        if inferred is util.Uninferable:
20✔
264
            continue
20✔
265
        if (
20✔
266
            (isinstance(inferred, nodes.Const) and bool(inferred.value))
267
            or (isinstance(inferred, (nodes.List, nodes.Tuple, nodes.Set)) and bool(inferred.elts))
268
            or (isinstance(inferred, nodes.Dict) and bool(inferred.items))
269
            or (isinstance(inferred, CONST_NODES))
270
        ):
271
            return True
20✔
272
    return False
20✔
273

274

275
def register(linter: PyLinter) -> None:
20✔
276
    linter.register_checker(InfiniteLoopChecker(linter))
20✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc