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

pyta-uoft / pyta / 16728746735

04 Aug 2025 04:30PM UTC coverage: 92.958% (-0.7%) from 93.654%
16728746735

Pull #1212

github

web-flow
Merge a3603fe50 into 92c14ad13
Pull Request #1212: Addition of new check to InfiniteLoopChecker

57 of 89 new or added lines in 1 file covered. (64.04%)

7 existing lines in 1 file now uncovered.

3538 of 3806 relevant lines covered (92.96%)

17.55 hits per line

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

70.64
/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_comp_test(node):
63
            return
20✔
64

65
        inferred = utils.safe_infer(node.test)
20✔
66
        if isinstance(inferred, util.UninferableBase) or inferred is None:
20✔
NEW
67
            return
×
68
        if isinstance(inferred, nodes.Const) and bool(inferred.value) is False:
20✔
NEW
69
            return
×
70

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

93
    def _check_constant_comp_test(self, node: nodes.While) -> bool:
20✔
94
        """Helper function that checks if while loop condition is a comparison of constant nodes."""
95
        cond_vars = set(child for child in node.test.nodes_of_class(nodes.Name))
20✔
96
        if cond_vars:
20✔
97
            return False
20✔
98
        inferred = utils.safe_infer(node.test)
20✔
99
        if isinstance(inferred, util.UninferableBase) or inferred is None:
20✔
NEW
100
            return False
×
101
        if isinstance(inferred, nodes.Const) and inferred.value is True:
20✔
102
            return True
20✔
NEW
103
        return False
×
104

105
    def _check_constant_loop_cond(
20✔
106
        self, node: nodes.While, test_node: Optional[nodes.NodeNG]
107
    ) -> bool:
108
        """Helper function that checks if while loop condition is constant.
109

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

139
        # Emit if calling a function that only returns GeneratorExp (always tests True)
140
        elif isinstance(test_node, nodes.Call):
20✔
141
            maybe_generator_call = test_node
20✔
142

143
        if maybe_generator_call:
20✔
144
            inferred_call = utils.safe_infer(maybe_generator_call.func)
20✔
145
            if isinstance(inferred_call, nodes.FunctionDef):
20✔
146
                # Can't use all(x) or not any(not x) for this condition, because it
147
                # will return True for empty generators, which is not what we want.
NEW
148
                all_returns_were_generator = None
×
NEW
149
                for return_node in inferred_call._get_return_nodes_skip_functions():
×
NEW
150
                    if not isinstance(return_node.value, nodes.GeneratorExp):
×
NEW
151
                        all_returns_were_generator = False
×
NEW
152
                        break
×
NEW
153
                    all_returns_were_generator = True
×
NEW
154
                if all_returns_were_generator:
×
NEW
155
                    return True
×
156
        if emit:
20✔
157
            return True
20✔
158
        elif isinstance(inferred, const_nodes):
20✔
159
            # If the constant node is a FunctionDef or Lambda then
160
            # it may be an illicit function call due to missing parentheses
161
            call_inferred = None
20✔
162
            try:
20✔
163
                # Just forcing the generator to infer all elements.
164
                # astroid.exceptions.InferenceError are false positives
165
                if isinstance(inferred, nodes.FunctionDef):
20✔
166
                    call_inferred = list(inferred.infer_call_result(node))
20✔
NEW
167
                elif isinstance(inferred, nodes.Lambda):
×
NEW
168
                    call_inferred = list(inferred.infer_call_result(node))
×
NEW
169
            except InferenceError:
×
NEW
170
                call_inferred = None
×
171
            if call_inferred:
20✔
172
                return True
20✔
NEW
173
            return True
×
174
        return False
20✔
175

176
    @staticmethod
20✔
177
    def _name_holds_generator(test_node: nodes.Name) -> tuple[bool, Optional[nodes.Call]]:
20✔
178
        """Return whether `test` tests a name certain to hold a generator, or optionally
179
        a call that should be then tested to see if *it* returns only generators.
180
        """
NEW
181
        assert isinstance(test_node, nodes.Name)
×
NEW
182
        emit = False
×
NEW
183
        maybe_generator_call = None
×
NEW
184
        lookup_result = test_node.frame().lookup(test_node.name)
×
NEW
185
        if not lookup_result:
×
NEW
186
            return emit, maybe_generator_call
×
NEW
187
        maybe_generator_assigned = (
×
188
            isinstance(assign_name.parent.value, nodes.GeneratorExp)
189
            for assign_name in lookup_result[1]
190
            if isinstance(assign_name.parent, nodes.Assign)
191
        )
NEW
UNCOV
192
        first_item = next(maybe_generator_assigned, None)
×
NEW
UNCOV
193
        if first_item is not None:
×
194
            # Emit if this variable is certain to hold a generator
NEW
UNCOV
195
            if all(itertools.chain((first_item,), maybe_generator_assigned)):
×
NEW
UNCOV
196
                emit = True
×
197
            # If this variable holds the result of a call, save it for next test
NEW
UNCOV
198
            elif (
×
199
                len(lookup_result[1]) == 1
200
                and isinstance(lookup_result[1][0].parent, nodes.Assign)
201
                and isinstance(lookup_result[1][0].parent.value, nodes.Call)
202
            ):
NEW
UNCOV
203
                maybe_generator_call = lookup_result[1][0].parent.value
×
NEW
UNCOV
204
        return emit, maybe_generator_call
×
205

206

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