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

pyta-uoft / pyta / 16975542374

14 Aug 2025 08:08PM UTC coverage: 93.252% (-0.3%) from 93.565%
16975542374

Pull #1212

github

web-flow
Merge e1add4c31 into b4041253e
Pull Request #1212: Addition of new check to InfiniteLoopChecker

58 of 71 new or added lines in 1 file covered. (81.69%)

1 existing line in 1 file now uncovered.

3524 of 3779 relevant lines covered (93.25%)

17.69 hits per line

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

85.71
/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

12
class InfiniteLoopChecker(BaseChecker):
20✔
13
    name = "infinite-loop"
20✔
14
    msgs = {
20✔
15
        "W9501": (
16
            "Infinite while loop: loop does not terminate",
17
            "infinite-loop",
18
            """Used when a while loop is guaranteed to never terminate based on its current structure. This usually
19
            indicates a logical error leading to an unintended infinite loop.""",
20
        ),
21
    }
22

23
    def visit_while(self, node: nodes.While) -> None:
20✔
24
        self._check_condition_constant(node)
20✔
25
        self._check_condition_all_var_used(node)
20✔
26

27
    def _check_condition_all_var_used(self, node: nodes.While) -> None:
20✔
28
        """Helper function that checks whether variables used in a while loop's condition
29
        are also used anywhere inside the loop body.
30

31
        Note: This is a basic check. It only flags loops where NONE of the condition variables
32
        appear in the body, which indicates an infinite loop.
33
        """
34
        # Get variable(s) used inside condition
35
        cond_vars = set()
20✔
36
        for child in node.test.nodes_of_class(nodes.Name):
20✔
37
            if not isinstance(child.parent, nodes.Call) or child.parent.func is not child:
20✔
38
                cond_vars.add(child.name)
20✔
39
        if not cond_vars:
20✔
40
            return
20✔
41
        # Check to see if condition variable(s) used inside body
42
        for child in node.body:
20✔
43
            for name_node in child.nodes_of_class((nodes.Name, nodes.AssignName)):
20✔
44
                if name_node.name in cond_vars:
20✔
45
                    # At least one condition variable is used in the loop body
46
                    return
20✔
47
        else:
48
            self.add_message(
20✔
49
                "infinite-loop",
50
                node=node.test,
51
            )
52

53
    def _check_condition_constant(self, node: nodes.While) -> None:
20✔
54
        """Helper function that checks if a constant while-loop condition may lead to an infinite loop.
55

56
        This helper flags loops that meet **both** of the following criteria:
57
            - The `while` condition is constant (e.g., `while 1 < 2`)
58
            - The loop body contains no `return`, `break`, `raise`, `yield`, or `sys.exit()` calls
59
        """
60
        if not self._check_constant_loop_cond(
20✔
61
            node, node.test
62
        ) and not self._check_constant_form_condition(node):
63
            return
20✔
64
        inferred = utils.safe_infer(node.test)
20✔
65
        if isinstance(inferred, util.UninferableBase) or inferred is None:
20✔
66
            return
20✔
67
        if (
20✔
68
            (isinstance(inferred, nodes.Const) and bool(inferred.value) is False)
69
            or (isinstance(inferred, (nodes.List, nodes.Tuple, nodes.Set)) and not inferred.elts)
70
            or (isinstance(inferred, nodes.Dict) and not inferred.items)
71
        ):
72
            return
20✔
73

74
        check_nodes = (nodes.Break, nodes.Return, nodes.Raise, nodes.Yield)
20✔
75
        for child in node.body:
20✔
76
            for exit_node in child.nodes_of_class(klass=(nodes.Call, *check_nodes)):
20✔
77
                if isinstance(exit_node, check_nodes):
20✔
78
                    return
20✔
79
                # Check Call node to see if `sys.exit()` is called
80
                elif (
20✔
81
                    isinstance(exit_node, nodes.Call)
82
                    and isinstance(exit_node.func, nodes.Attribute)
83
                    and exit_node.func.attrname == "exit"
84
                ):
85
                    inferred = utils.safe_infer(exit_node.func.expr)
20✔
86
                    if (
20✔
87
                        not isinstance(inferred, util.UninferableBase)
88
                        and inferred is not None
89
                        and isinstance(inferred, nodes.Module)
90
                        and inferred.name == "sys"
91
                    ):
92
                        return
20✔
93
        else:
94
            self.add_message("infinite-loop", node=node.test, confidence=INFERENCE)
20✔
95

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

100
    def _check_constant_loop_cond(
20✔
101
        self, node: nodes.While, test_node: Optional[nodes.NodeNG]
102
    ) -> bool:
103
        """Helper function that checks if while loop condition is constant.
104

105
        See `https://github.com/pylint-dev/pylint/blob/main/pylint/checkers/base/basic_checker.py#L303` for further
106
        detail."""
107
        const_nodes = (
20✔
108
            nodes.Module,
109
            nodes.GeneratorExp,
110
            nodes.Lambda,
111
            nodes.FunctionDef,
112
            nodes.ClassDef,
113
            bases.Generator,
114
            UnboundMethod,
115
            BoundMethod,
116
        )
117
        structs = (nodes.Dict, nodes.Tuple, nodes.Set, nodes.List)
20✔
118
        except_nodes = (
20✔
119
            nodes.Call,
120
            nodes.BinOp,
121
            nodes.BoolOp,
122
            nodes.UnaryOp,
123
            nodes.Subscript,
124
        )
125
        inferred = None
20✔
126
        maybe_generator_call = None
20✔
127
        emit = isinstance(test_node, (nodes.Const, *structs, *const_nodes))
20✔
128
        if not isinstance(test_node, except_nodes):
20✔
129
            inferred = utils.safe_infer(test_node)
20✔
130
            # If we can't infer what the value is but the test is just a variable name
131
            if isinstance(inferred, util.UninferableBase) and isinstance(test_node, nodes.Name):
20✔
132
                emit, maybe_generator_call = InfiniteLoopChecker._name_holds_generator(test_node)
20✔
133

134
        # Emit if calling a function that only returns GeneratorExp (always tests True)
135
        elif isinstance(test_node, nodes.Call):
20✔
136
            maybe_generator_call = test_node
20✔
137

138
        if maybe_generator_call:
20✔
139
            inferred_call = utils.safe_infer(maybe_generator_call.func)
20✔
140
            if isinstance(inferred_call, nodes.FunctionDef):
20✔
141
                # Can't use all(x) or not any(not x) for this condition, because it
142
                # will return True for empty generators, which is not what we want.
NEW
143
                all_returns_were_generator = None
×
NEW
144
                for return_node in inferred_call._get_return_nodes_skip_functions():
×
NEW
145
                    if not isinstance(return_node.value, nodes.GeneratorExp):
×
NEW
146
                        all_returns_were_generator = False
×
NEW
147
                        break
×
NEW
148
                    all_returns_were_generator = True
×
NEW
149
                if all_returns_were_generator:
×
NEW
150
                    return True
×
151
        if emit:
20✔
152
            return True
20✔
153
        elif isinstance(inferred, const_nodes):
20✔
154
            return True
20✔
155
        return False
20✔
156

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

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

189

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