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

pyta-uoft / pyta / 9376151692

05 Jun 2024 12:29AM UTC coverage: 91.523% (-1.6%) from 93.12%
9376151692

push

github

web-flow
test: Refactor `test_check_on_dir` method (#1042)

* refactor: refactor test_check_on_dir method

Create a test suite sample_dir that contains a subset of test cases in examples.
Change test_check_on_dir() to run on sample_dir rather than the whole examples.

Fix the regular expression for _EXAMPLE_PREFIX_REGIX which previously does not match the file names.
Rename test cases whose file names do not match message symbols.
Modify regular expression for pycodestyle file names for more precise matching.
Add a separate test to check ModuleNameViolation.
Modify get_file_paths to handle nested directories correctly.

2807 of 3067 relevant lines covered (91.52%)

9.02 hits per line

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

91.61
/python_ta/transforms/setendings.py
1
"""
5✔
2
Top-level functions to mutate the astroid nodes with `end_col_offset` and
3
`end_lineno` properties.
4

5
Where possible, the `end_col_offset` property is set by that of the node's last child.
6

7
    fromlineno
8
        - existing attribute
9
        - one-indexed
10
    end_lineno
11
        - new attribute
12
        - one-indexed
13
    col_offset
14
        - existing attribute
15
        - zero-indexed
16
        - located left of the first character
17
    end_col_offset
18
        - new attribute
19
        - zero-indexed
20
        - located right of the last character (essentially the string length)
21

22
In astroid/astroid/transforms.py, functions are registered to types in the
23
`transforms` dictionary in the TransformVisitor class. The traversal at
24
line 83 eventually leads to the transform called on each node at line 36,
25
within the _transform method.
26

27
Astroid Source:
28
https://github.com/PyCQA/astroid/blob/master/astroid/transforms.py
29
"""
30

31
from astroid import nodes
10✔
32
from astroid.transforms import TransformVisitor
10✔
33

34
CONSUMABLES = " \n\t\\"
10✔
35

36

37
# These nodes have no children, and their end_lineno and end_col_offset
38
# attributes are set based on their string representation (according to astroid).
39
# Goal: eventually replace the transforms for all the nodes in this list with the
40
# predicate technique that uses more robust approach using searching, rather than
41
# simple length of string.
42
NODES_WITHOUT_CHILDREN = [
10✔
43
    nodes.AssignName,
44
    nodes.Break,
45
    nodes.Const,
46
    nodes.Continue,
47
    nodes.DelName,
48
    nodes.Global,
49
    nodes.Import,
50
    nodes.ImportFrom,
51
    nodes.Name,
52
    nodes.Nonlocal,
53
    nodes.Pass,
54
    nodes.Yield,
55
]
56

57
# These nodes have a child, and their end_lineno and end_col_offset
58
# attributes are set equal to those of their last child.
59
NODES_WITH_CHILDREN = [
10✔
60
    nodes.Assert,
61
    nodes.Assign,
62
    nodes.AsyncFor,
63
    nodes.AsyncFunctionDef,
64
    nodes.AsyncWith,
65
    nodes.AugAssign,
66
    nodes.Await,
67
    nodes.BinOp,
68
    nodes.BoolOp,
69
    nodes.Call,
70
    nodes.ClassDef,
71
    nodes.Compare,
72
    nodes.Comprehension,
73
    nodes.Decorators,
74
    nodes.Delete,
75
    nodes.ExceptHandler,
76
    nodes.For,
77
    nodes.FormattedValue,
78
    nodes.FunctionDef,
79
    nodes.GeneratorExp,
80
    nodes.If,
81
    nodes.IfExp,
82
    nodes.Keyword,
83
    nodes.Lambda,
84
    nodes.List,
85
    nodes.Module,
86
    nodes.Raise,
87
    nodes.Return,
88
    nodes.Starred,
89
    nodes.Subscript,
90
    nodes.Try,
91
    nodes.UnaryOp,
92
    nodes.While,
93
    nodes.With,
94
    nodes.YieldFrom,
95
]
96

97

98
# Predicate functions, for setting locations based on source code.
99
# Predicates can only return a single truthy value, because of how its used in
100
# `astroid/transforms.py`
101
# ====================================================
102
def _token_search(token):
10✔
103
    """
104
    @type token: string
105
    @rtype: function
106
    """
107

108
    def _is_token(s, index, node):
10✔
109
        """Fix to include certain tokens such as a paren, bracket, or brace.
110
        @type s: string
111
        @type index: int
112
        @type node: Astroid node
113
        @rtype: bool
114
        """
115
        return s[index] == token
10✔
116

117
    return _is_token
10✔
118

119

120
def _keyword_search(keyword):
10✔
121
    """
122
    @type keyword: string
123
    @rtype: function
124
    """
125

126
    def _is_keyword(s, index, node):
10✔
127
        """Search for a keyword. Right-to-left.
128
        @type s: string
129
        @type index: int
130
        @type node: Astroid node
131
        @rtype: bool
132
        """
133
        return s[index : index + len(keyword)] == keyword
10✔
134

135
    return _is_keyword
10✔
136

137

138
def _is_arg_name(s, index, node):
10✔
139
    """Search for the name of the argument. Right-to-left."""
140
    if not node.arg:
10✔
141
        return False
10✔
142
    return s[index : index + len(node.arg)] == node.arg
10✔
143

144

145
# Nodes the require the source code for proper location setting
146
# Elements here are in the form
147
# (node class, predicate for start | None, predicate for end | None)
148
NODES_REQUIRING_SOURCE = [
10✔
149
    (nodes.AsyncFor, _keyword_search("async"), None),
150
    (nodes.AsyncFunctionDef, _keyword_search("async"), None),
151
    (nodes.AsyncWith, _keyword_search("async"), None),
152
    (nodes.Call, None, _token_search(")")),
153
    (nodes.DelAttr, _keyword_search("del"), None),
154
    (nodes.DelName, _keyword_search("del"), None),
155
    (nodes.Dict, None, _token_search("}")),
156
    (nodes.DictComp, None, _token_search("}")),
157
    (nodes.Expr, _token_search("("), _token_search(")")),
158
    (nodes.GeneratorExp, _token_search("("), _token_search(")")),
159
    (nodes.If, _keyword_search("elif"), None),
160
    (nodes.Keyword, _is_arg_name, None),
161
    (nodes.List, _token_search("["), _token_search("]")),
162
    (nodes.ListComp, _token_search("["), _token_search("]")),
163
    (nodes.Set, None, _token_search("}")),
164
    (nodes.SetComp, None, _token_search("}")),
165
    (nodes.Slice, _token_search("["), None),
166
    (nodes.Tuple, None, _token_search(",")),
167
]
168

169

170
def init_register_ending_setters(source_code):
10✔
171
    """Instantiate a visitor to transform the nodes.
172
    Register the transform functions on an instance of TransformVisitor.
173

174
    @type source_code: list of strings
175
    @rtype: TransformVisitor
176
    """
177
    ending_transformer = TransformVisitor()
10✔
178

179
    # Check consistency of astroid-provided fromlineno and col_offset attributes.
180
    for node_class in nodes.ALL_NODE_CLASSES:
10✔
181
        ending_transformer.register_transform(
10✔
182
            node_class,
183
            fix_start_attributes,
184
            lambda node: (
185
                getattr(node, "fromlineno", None) is None
186
                or getattr(node, "col_offset", None) is None
187
            ),
188
        )
189

190
    # Ad hoc transformations
191
    ending_transformer.register_transform(nodes.BinOp, _set_start_from_first_child)
10✔
192
    ending_transformer.register_transform(nodes.ClassDef, _set_start_from_first_decorator)
10✔
193
    ending_transformer.register_transform(nodes.FunctionDef, _set_start_from_first_decorator)
10✔
194
    ending_transformer.register_transform(nodes.Tuple, _set_start_from_first_child)
10✔
195
    ending_transformer.register_transform(nodes.Arguments, fix_arguments(source_code))
10✔
196
    ending_transformer.register_transform(nodes.Slice, fix_slice(source_code))
10✔
197

198
    for node_class in NODES_WITHOUT_CHILDREN:
10✔
199
        ending_transformer.register_transform(node_class, set_without_children)
10✔
200
    for node_class in NODES_WITH_CHILDREN:
10✔
201
        ending_transformer.register_transform(node_class, set_from_last_child)
10✔
202

203
    ending_transformer.register_transform(nodes.Subscript, fix_subscript(source_code))
10✔
204

205
    # Nodes where the source code must also be provided.
206
    # source_code and the predicate functions get stored in the TransformVisitor
207
    for node_class, start_pred, end_pred in NODES_REQUIRING_SOURCE:
10✔
208
        if start_pred is not None:
10✔
209
            ending_transformer.register_transform(
10✔
210
                node_class, start_setter_from_source(source_code, start_pred)
211
            )
212
        if end_pred is not None:
10✔
213
            # This is for searching for a trailing comma after a tuple's final element
214
            if node_class is nodes.Tuple:
10✔
215
                ending_transformer.register_transform(
10✔
216
                    node_class, end_setter_from_source(source_code, end_pred, True)
217
                )
218
            else:
219
                ending_transformer.register_transform(
10✔
220
                    node_class, end_setter_from_source(source_code, end_pred)
221
                )
222

223
    # Nodes where extra parentheses are included
224
    ending_transformer.register_transform(nodes.BinOp, add_parens(source_code))
10✔
225
    ending_transformer.register_transform(nodes.Const, add_parens(source_code))
10✔
226
    ending_transformer.register_transform(nodes.Tuple, add_parens(source_code))
10✔
227

228
    return ending_transformer
10✔
229

230

231
# Transform functions.
232
# These functions are called on individual nodes to either fix the
233
# `fromlineno` and `col_offset` properties of the nodes,
234
# or to set the `end_lineno` and `end_col_offset` attributes for a node.
235
# ====================================================
236
def fix_slice(source_code):
10✔
237
    """
238
    The Slice node column positions are mostly set properly when it has (Const)
239
    children. The main problem is when Slice node doesn't have children.
240
    E.g "[:]", "[::]", "[:][:]", "[::][::]", ... yikes! The existing positions
241
    are sometimes set improperly to 0.
242
    """
243

244
    def _find_square_brackets(node):
10✔
245
        if _get_last_child(node):
10✔
246
            set_from_last_child(node)
10✔
247
            line_i = node.end_lineno - 1  # convert 1 to 0 index.
10✔
248
            char_i = node.end_col_offset
10✔
249
            has_children = True
10✔
250
        else:
251
            line_i = node.parent.value.end_lineno - 1  # convert 1 to 0 index.
10✔
252
            char_i = node.parent.value.end_col_offset
10✔
253
            has_children = False
10✔
254

255
        # Search the remaining source code for the "]" char.
256
        while (
10✔
257
            line_i < len(source_code)
258
            and char_i < len(source_code[line_i])
259
            and source_code[line_i][char_i] != "]"
260
        ):
261
            if char_i == len(source_code[line_i]) - 1 or source_code[line_i][char_i] == "#":
10✔
262
                char_i = 0
×
263
                line_i += 1
×
264
            else:
265
                char_i += 1
10✔
266

267
        if not has_children:
10✔
268
            node.fromlineno, node.col_offset = line_i + 1, char_i
10✔
269
        node.end_lineno, node.end_col_offset = line_i + 1, char_i + 1
10✔
270

271
        return node
10✔
272

273
    return _find_square_brackets
10✔
274

275

276
def fix_subscript(source_code):
10✔
277
    """For a Subscript node.
278

279
    Need to include this because the index/extended slice is a value rather than
280
    a separate Index/ExtSlice in Python 3.9.
281
    """
282

283
    def _fix_end(node: nodes.Subscript) -> nodes.Subscript:
10✔
284
        if isinstance(node.slice, (nodes.Slice, nodes.Tuple)):
10✔
285
            # In this case, the subscript node already contains the final ].
286
            return node
10✔
287

288
        # Search the remaining source code for the "]" char.
289
        if _get_last_child(node):
10✔
290
            set_from_last_child(node)
10✔
291
            line_i = node.end_lineno - 1  # convert 1 to 0 index.
10✔
292
            char_i = node.end_col_offset
10✔
293
        else:
294
            line_i = node.value.end_lineno - 1  # convert 1 to 0 index.
×
295
            char_i = node.value.end_col_offset
×
296

297
        while char_i < len(source_code[line_i]) and source_code[line_i][char_i] != "]":
10✔
298
            if char_i == len(source_code[line_i]) - 1 or source_code[line_i][char_i] == "#":
10✔
299
                char_i = 0
×
300
                line_i += 1
×
301
            else:
302
                char_i += 1
10✔
303

304
        node.end_lineno, node.end_col_offset = line_i + 1, char_i + 1
10✔
305
        return node
10✔
306

307
    return _fix_end
10✔
308

309

310
def fix_arguments(source_code):
10✔
311
    """For an Arguments node"""
312

313
    def _find(node: nodes.Arguments) -> nodes.Arguments:
10✔
314
        children = list(node.get_children())
10✔
315
        if children:
10✔
316
            fix_start_attributes(node)
10✔
317

318
        line_i = node.parent.fromlineno
10✔
319
        char_i = node.parent.col_offset
10✔
320
        for child in children:
10✔
321
            if line_i is None:
10✔
322
                line_i = child.end_lineno
×
323
                char_i = child.end_col_offset
×
324
            elif (
10✔
325
                line_i < child.end_lineno
326
                or line_i == child.end_lineno
327
                and char_i < child.end_col_offset
328
            ):
329
                line_i = child.end_lineno
10✔
330
                char_i = child.end_col_offset
10✔
331

332
        line_i -= 1  # Switch to 0-indexing
10✔
333

334
        # left bracket if parent is FunctionDef, colon if Lambda
335
        if isinstance(node.parent, nodes.FunctionDef):
10✔
336
            end_char = ")"
10✔
337
        else:
338
            end_char = ":"
10✔
339

340
        while char_i < len(source_code[line_i]) and source_code[line_i][char_i] != end_char:
10✔
341
            if char_i == len(source_code[line_i]) - 1 or source_code[line_i][char_i] == "#":
10✔
342
                char_i = 0
10✔
343
                line_i += 1
10✔
344
            else:
345
                char_i += 1
10✔
346

347
        node.end_lineno, node.end_col_offset = line_i + 1, char_i
10✔
348

349
        # no children
350
        if children == []:
10✔
351
            node.fromlineno, node.col_offset = line_i + 1, char_i
10✔
352

353
        return node
10✔
354

355
    return _find
10✔
356

357

358
def fix_start_attributes(node):
10✔
359
    """Some nodes don't always have the `col_offset` property set by Astroid:
360
    Comprehension, Keyword, Module, Slice.
361
    """
362
    try:
10✔
363
        first_child = next(node.get_children())
10✔
364
        if getattr(node, "fromlineno", None) is None:
10✔
365
            node.fromlineno = first_child.fromlineno
×
366
        if getattr(node, "col_offset", None) is None:
10✔
367
            node.col_offset = first_child.col_offset
10✔
368

369
    except StopIteration:
10✔
370
        # No children. Go to the enclosing statement and use that.
371
        # This assumes that statement nodes will always have these attributes set.
372
        statement = node.statement()
10✔
373
        if statement is not node:
10✔
374
            if getattr(node, "fromlineno", None) is None:
10✔
375
                node.fromlineno = statement.fromlineno
×
376
            if getattr(node, "col_offset", None) is None:
10✔
377
                node.col_offset = statement.col_offset
10✔
378
        else:
379
            # Enclosing statement is same as node, also does not have attributes set
380
            if getattr(node, "fromlineno", None) is None:
×
381
                node.fromlineno = 0
×
382
            if getattr(node, "col_offset", None) is None:
×
383
                node.col_offset = 0
×
384
    return node
10✔
385

386

387
def _set_start_from_first_child(node):
10✔
388
    """Set the start attributes of this node from its first child."""
389
    try:
10✔
390
        first_child = next(node.get_children())
10✔
391
    except StopIteration:
10✔
392
        pass
10✔
393
    else:
394
        node.fromlineno = first_child.fromlineno
10✔
395
        node.col_offset = first_child.col_offset
10✔
396
    return node
10✔
397

398

399
def _set_start_from_first_decorator(node):
10✔
400
    """Set the start attributes of this node from its first child, if that child is a decorator."""
401
    if getattr(node, "decorators"):
10✔
402
        first_child = node.decorators
10✔
403
        node.fromlineno = first_child.fromlineno
10✔
404
        node.col_offset = first_child.col_offset
10✔
405
    return node
10✔
406

407

408
def set_from_last_child(node):
10✔
409
    """Populate ending locations for astroid node based on its last child.
410

411
    Preconditions:
412
      - `node` must have a `last_child` (node).
413
      - `node` has col_offset property set.
414
    """
415
    last_child = _get_last_child(node)
10✔
416
    if not last_child:
10✔
417
        set_without_children(node)
10✔
418
        return node
10✔
419
    elif not hasattr(last_child, "end_lineno"):  # Newly added for Slice() node.
10✔
420
        set_without_children(last_child)
×
421

422
    if last_child.end_lineno is not None:
10✔
423
        node.end_lineno = last_child.end_lineno
10✔
424
    if last_child.end_col_offset is not None:
10✔
425
        node.end_col_offset = last_child.end_col_offset
10✔
426
    return node
10✔
427

428

429
def set_without_children(node):
10✔
430
    """Populate ending locations for nodes that are guaranteed to never have
431
    children. E.g. Const.
432

433
    These node's end_col_offset are currently assigned based on their
434
    computed string representation. This may differ from their actual
435
    source code representation, however (mainly whitespace).
436

437
    Precondition: `node` must not have a `last_child` (node).
438
    """
439
    if not hasattr(node, "end_lineno"):
10✔
440
        node.end_lineno = node.fromlineno
×
441
    # FIXME: using the as_string() is a bad technique because many different
442
    # whitespace possibilities that may not be reflected in it!
443
    if not hasattr(node, "end_col_offset"):
10✔
444
        node.end_col_offset = node.col_offset + len(node.as_string())
×
445
    return node
10✔
446

447

448
def _get_last_child(node):
10✔
449
    """Returns the last child node, or None.
450
    Some nodes' last_child() attribute not set, e.g. nodes.Arguments.
451
    """
452
    if node.last_child():
10✔
453
        return node.last_child()
10✔
454
    else:
455
        # Get the first child from the `get_children` generator.
456
        skip_to_last_child = None  # save reference to last child.
10✔
457
        for skip_to_last_child in node.get_children():
10✔
458
            pass  # skip to last
×
459
        return skip_to_last_child  # postcondition: node, or None.
10✔
460

461

462
def end_setter_from_source(source_code, pred, only_consumables=False):
10✔
463
    """Returns a *function* that sets ending locations for a node from source.
464

465
    The basic technique is to do the following:
466
      1. Find the ending locations for the node based on its last child.
467
      2. Starting at that point, iterate through characters in the source code
468
         up to and including the first index that satisfies pred.
469

470
    pred is a function that takes a string and index and returns a bool,
471
    e.g. _is_close_paren
472

473
    If only_consumables is True, the search halts when it reaches a non-consumable
474
    character that fails pred *on the first line*.
475
    TODO: really the behaviour should be the same for all lines searched for.
476
    """
477

478
    def set_endings_from_source(node):
10✔
479
        # Tuple nodes have an end_col_offset that includes the end paren,
480
        # but their col_offset does not include the start paren.
481
        # To address this, we override the Tuple node's end_col_offset.
482
        if not hasattr(node, "end_col_offset") or isinstance(node, nodes.Tuple):
10✔
483
            set_from_last_child(node)
10✔
484

485
        # Initialize counters. Note: we need to offset lineno,
486
        # since it's 1-indexed.
487
        end_col_offset, lineno = node.end_col_offset, node.end_lineno - 1
10✔
488

489
        # First, search the remaining part of the current end line.
490
        for j in range(end_col_offset, len(source_code[lineno])):
10✔
491
            if source_code[lineno][j] == "#":
10✔
492
                break  # skip over comment lines
10✔
493
            if pred(source_code[lineno], j, node):
10✔
494
                node.end_col_offset = j + 1
10✔
495
                return node
10✔
496
            elif only_consumables and source_code[lineno][j] not in CONSUMABLES:
10✔
497
                return node
10✔
498

499
        # If that doesn't work, search remaining lines
500
        for i in range(lineno + 1, len(source_code)):
10✔
501
            # Search each character
502
            for j in range(len(source_code[i])):
10✔
503
                if source_code[i][j] == "#":
10✔
504
                    break  # skip over comment lines
10✔
505
                if pred(source_code[i], j, node):
10✔
506
                    node.end_col_offset, node.end_lineno = j + 1, i + 1
10✔
507
                    return node
10✔
508
                # only consume inert characters.
509
                elif source_code[i][j] not in CONSUMABLES:
10✔
510
                    return node
10✔
511
        return node
10✔
512

513
    return set_endings_from_source
10✔
514

515

516
def start_setter_from_source(source_code, pred):
10✔
517
    """Returns a *function* that sets start locations for a node from source.
518
    Recall `source_code`, `pred` are within the lexical scope of the returned function.
519

520
    The basic technique is to do the following:
521
      1. Find the start locations for the node (already set).
522
      2. Starting at that point, iterate through characters in the source code
523
         in reverse until reaching the first index that satisfies pred.
524

525
    pred is a function that takes a string and index and returns a bool,
526
    e.g. _is_open_paren
527
    """
528

529
    def set_start_from_source(node):
10✔
530
        # Initialize counters. Note: fromlineno is 1-indexed.
531
        col_offset, lineno = node.col_offset, node.fromlineno - 1
10✔
532

533
        # First, search the remaining part of the current start line
534
        for j in range(min(len(source_code[lineno]) - 1, col_offset), -1, -1):
10✔
535
            if pred(source_code[lineno], j, node):
10✔
536
                node.col_offset = j
10✔
537
                return node
10✔
538

539
        # If that doesn't work, search remaining lines
540
        for i in range(lineno - 1, -1, -1):
10✔
541
            # Search each character, right-to-left
542
            for j in range(len(source_code[i]) - 1, -1, -1):
10✔
543
                if pred(source_code[i], j, node):
10✔
544
                    node.end_col_offset, node.end_lineno = j, i + 1
×
545
                    return node
×
546
                # only consume inert characters.
547
                elif source_code[i][j] not in CONSUMABLES:
10✔
548
                    return node
10✔
549
        return node
10✔
550

551
    return set_start_from_source
10✔
552

553

554
def add_parens(source_code):
10✔
555
    def h(node):
10✔
556
        _add_parens(source_code)(node)
10✔
557

558
    return h
10✔
559

560

561
def _add_parens(source_code):
10✔
562
    def h(node):
10✔
563
        # Initialize counters. Note: fromlineno is 1-indexed.
564
        prev = node.fromlineno, node.col_offset, node.end_lineno, node.end_col_offset
10✔
565
        while True:
6✔
566
            col_offset, lineno = node.col_offset, node.fromlineno - 1
10✔
567
            end_col_offset, end_lineno = node.end_col_offset, node.end_lineno - 1
10✔
568

569
            # First, search the remaining part of the current start line
570
            prev_char, new_lineno, new_coloffset = None, None, None
10✔
571
            for j in range(col_offset - 1, -1, -1):
10✔
572
                if source_code[lineno][j] in CONSUMABLES or source_code[lineno][j] == ",":
10✔
573
                    continue
10✔
574
                else:
575
                    prev_char, new_lineno, new_coloffset = source_code[lineno][j], lineno, j
10✔
576
                    break
10✔
577

578
            if prev_char is None:
10✔
579
                # Search remaining lines
580
                for i in range(lineno - 1, -1, -1):
10✔
581
                    # Search each character, right-to-left
582
                    for j in range(len(source_code[i]) - 1, -1, -1):
10✔
583
                        if source_code[i][j] in CONSUMABLES or source_code[i][j] == ",":
10✔
584
                            continue
10✔
585
                        else:
586
                            prev_char, new_lineno, new_coloffset = source_code[i][j], i, j
10✔
587

588
                            break
10✔
589
                    if prev_char is not None:
10✔
590
                        break
10✔
591

592
            if prev_char != "(":
10✔
593
                # No enclosing parentheses
594
                break
10✔
595

596
            # Now search for matching ')'
597
            next_char, new_end_lineno, new_end_coloffset = None, None, None
10✔
598
            for j in range(end_col_offset, len(source_code[end_lineno])):
10✔
599
                if source_code[end_lineno][j] == "#":
10✔
600
                    break  # skip over comment lines
×
601
                elif source_code[end_lineno][j] in CONSUMABLES:
10✔
602
                    continue
10✔
603
                else:
604
                    next_char, new_end_lineno, new_end_coloffset = (
10✔
605
                        source_code[end_lineno][j],
606
                        end_lineno,
607
                        j,
608
                    )
609
                    break
10✔
610

611
            if next_char is None:
10✔
612
                # Search remaining lines
613
                for i in range(end_lineno + 1, len(source_code)):
10✔
614
                    # Search each character
615
                    for j in range(len(source_code[i])):
10✔
616
                        if source_code[i][j] == "#":
10✔
617
                            break  # skip over comment lines
×
618
                        elif source_code[i][j] in CONSUMABLES:
10✔
619
                            continue
×
620
                        else:
621
                            next_char, new_end_lineno, new_end_coloffset = source_code[i][j], i, j
10✔
622
                            break
10✔
623
                    if next_char is not None:
10✔
624
                        break
10✔
625

626
            if next_char != ")":
10✔
627
                break
10✔
628

629
            # At this point, an enclosing pair of parentheses has been found
630
            prev = node.fromlineno, node.col_offset, node.end_lineno, node.end_col_offset
10✔
631
            node.fromlineno, node.col_offset, node.end_lineno, node.end_col_offset = (
10✔
632
                new_lineno + 1,
633
                new_coloffset,
634
                new_end_lineno + 1,
635
                new_end_coloffset + 1,
636
            )
637

638
        # Go back by 1 set of parentheses if inside a function call.
639
        if isinstance(node.parent, nodes.Call) and len(node.parent.args) == 1:
10✔
640
            node.fromlineno, node.col_offset, node.end_lineno, node.end_col_offset = prev
10✔
641

642
        return node
10✔
643

644
    return h
10✔
645

646

647
# Make this module a pylint plugin
648
def register(linter):
10✔
649
    """Patch linter to apply message transform with source code."""
650
    old_get_ast = linter.get_ast
10✔
651

652
    def new_get_ast(filepath, modname, data):
10✔
653
        ast = old_get_ast(filepath, modname, data)
10✔
654
        if ast is not None:
10✔
655
            with open(filepath, encoding="utf-8") as f:
10✔
656
                source_code = f.readlines()
10✔
657
            ending_transformer = init_register_ending_setters(source_code)
10✔
658
            ending_transformer.visit(ast)
10✔
659
        return ast
10✔
660

661
    linter.get_ast = new_get_ast
10✔
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