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

pyta-uoft / pyta / 22284929430

22 Feb 2026 08:41PM UTC coverage: 89.979% (-0.005%) from 89.984%
22284929430

push

github

web-flow
Removed extraneous print calls in InfiniteLoopChecker (#1308)

3448 of 3832 relevant lines covered (89.98%)

17.45 hits per line

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

99.28
/packages/python-ta/src/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.checkers.utils import only_required_for_messages
20✔
9
from pylint.interfaces import INFERENCE
20✔
10
from pylint.lint import PyLinter
20✔
11

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

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

34

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

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

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

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

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

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

94
        inferred = infer_condition(node)
20✔
95
        if not inferred:
20✔
96
            return False
20✔
97

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

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

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

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

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

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

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

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

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

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

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

240

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

247

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

256

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

276

277
def register(linter: PyLinter) -> None:
20✔
278
    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