Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
marvel
GitHub Repository: marvel/qnf
Path: blob/master/elisp/emacs-for-python/rope-dist/rope/refactor/extract.py
1415 views
1
import re
2
3
from rope.base import ast, codeanalyze
4
from rope.base.change import ChangeSet, ChangeContents
5
from rope.base.exceptions import RefactoringError
6
from rope.refactor import (sourceutils, similarfinder,
7
patchedast, suites, usefunction)
8
9
10
# Extract refactoring has lots of special cases. I tried to split it
11
# to smaller parts to make it more manageable:
12
#
13
# _ExtractInfo: holds information about the refactoring; it is passed
14
# to the parts that need to have information about the refactoring
15
#
16
# _ExtractCollector: merely saves all of the information necessary for
17
# performing the refactoring.
18
#
19
# _DefinitionLocationFinder: finds where to insert the definition.
20
#
21
# _ExceptionalConditionChecker: checks for exceptional conditions in
22
# which the refactoring cannot be applied.
23
#
24
# _ExtractMethodParts: generates the pieces of code (like definition)
25
# needed for performing extract method.
26
#
27
# _ExtractVariableParts: like _ExtractMethodParts for variables.
28
#
29
# _ExtractPerformer: Uses above classes to collect refactoring
30
# changes.
31
#
32
# There are a few more helper functions and classes used by above
33
# classes.
34
class _ExtractRefactoring(object):
35
36
def __init__(self, project, resource, start_offset, end_offset,
37
variable=False):
38
self.project = project
39
self.pycore = project.pycore
40
self.resource = resource
41
self.start_offset = self._fix_start(resource.read(), start_offset)
42
self.end_offset = self._fix_end(resource.read(), end_offset)
43
44
def _fix_start(self, source, offset):
45
while offset < len(source) and source[offset].isspace():
46
offset += 1
47
return offset
48
49
def _fix_end(self, source, offset):
50
while offset > 0 and source[offset - 1].isspace():
51
offset -= 1
52
return offset
53
54
def get_changes(self, extracted_name, similar=False, global_=False):
55
"""Get the changes this refactoring makes
56
57
:parameters:
58
- `similar`: if `True`, similar expressions/statements are also
59
replaced.
60
- `global_`: if `True`, the extracted method/variable will
61
be global.
62
63
"""
64
info = _ExtractInfo(
65
self.project, self.resource, self.start_offset, self.end_offset,
66
extracted_name, variable=self.kind == 'variable',
67
similar=similar, make_global=global_)
68
new_contents = _ExtractPerformer(info).extract()
69
changes = ChangeSet('Extract %s <%s>' % (self.kind,
70
extracted_name))
71
changes.add_change(ChangeContents(self.resource, new_contents))
72
return changes
73
74
75
class ExtractMethod(_ExtractRefactoring):
76
77
def __init__(self, *args, **kwds):
78
super(ExtractMethod, self).__init__(*args, **kwds)
79
80
kind = 'method'
81
82
83
class ExtractVariable(_ExtractRefactoring):
84
85
def __init__(self, *args, **kwds):
86
kwds = dict(kwds)
87
kwds['variable'] = True
88
super(ExtractVariable, self).__init__(*args, **kwds)
89
90
kind = 'variable'
91
92
93
class _ExtractInfo(object):
94
"""Holds information about the extract to be performed"""
95
96
def __init__(self, project, resource, start, end, new_name,
97
variable, similar, make_global):
98
self.pycore = project.pycore
99
self.resource = resource
100
self.pymodule = self.pycore.resource_to_pyobject(resource)
101
self.global_scope = self.pymodule.get_scope()
102
self.source = self.pymodule.source_code
103
self.lines = self.pymodule.lines
104
self.new_name = new_name
105
self.variable = variable
106
self.similar = similar
107
self._init_parts(start, end)
108
self._init_scope()
109
self.make_global = make_global
110
111
def _init_parts(self, start, end):
112
self.region = (self._choose_closest_line_end(start),
113
self._choose_closest_line_end(end, end=True))
114
115
start = self.logical_lines.logical_line_in(
116
self.lines.get_line_number(self.region[0]))[0]
117
end = self.logical_lines.logical_line_in(
118
self.lines.get_line_number(self.region[1]))[1]
119
self.region_lines = (start, end)
120
121
self.lines_region = (self.lines.get_line_start(self.region_lines[0]),
122
self.lines.get_line_end(self.region_lines[1]))
123
124
@property
125
def logical_lines(self):
126
return self.pymodule.logical_lines
127
128
def _init_scope(self):
129
start_line = self.region_lines[0]
130
scope = self.global_scope.get_inner_scope_for_line(start_line)
131
if scope.get_kind() != 'Module' and scope.get_start() == start_line:
132
scope = scope.parent
133
self.scope = scope
134
self.scope_region = self._get_scope_region(self.scope)
135
136
def _get_scope_region(self, scope):
137
return (self.lines.get_line_start(scope.get_start()),
138
self.lines.get_line_end(scope.get_end()) + 1)
139
140
def _choose_closest_line_end(self, offset, end=False):
141
lineno = self.lines.get_line_number(offset)
142
line_start = self.lines.get_line_start(lineno)
143
line_end = self.lines.get_line_end(lineno)
144
if self.source[line_start:offset].strip() == '':
145
if end:
146
return line_start - 1
147
else:
148
return line_start
149
elif self.source[offset:line_end].strip() == '':
150
return min(line_end, len(self.source))
151
return offset
152
153
@property
154
def one_line(self):
155
return self.region != self.lines_region and \
156
(self.logical_lines.logical_line_in(self.region_lines[0]) ==
157
self.logical_lines.logical_line_in(self.region_lines[1]))
158
159
@property
160
def global_(self):
161
return self.scope.parent is None
162
163
@property
164
def method(self):
165
return self.scope.parent is not None and \
166
self.scope.parent.get_kind() == 'Class'
167
168
@property
169
def indents(self):
170
return sourceutils.get_indents(self.pymodule.lines,
171
self.region_lines[0])
172
173
@property
174
def scope_indents(self):
175
if self.global_:
176
return 0
177
return sourceutils.get_indents(self.pymodule.lines,
178
self.scope.get_start())
179
180
@property
181
def extracted(self):
182
return self.source[self.region[0]:self.region[1]]
183
184
_returned = None
185
@property
186
def returned(self):
187
"""Does the extracted piece contain return statement"""
188
if self._returned is None:
189
node = _parse_text(self.extracted)
190
self._returned = usefunction._returns_last(node)
191
return self._returned
192
193
194
class _ExtractCollector(object):
195
"""Collects information needed for performing the extract"""
196
197
def __init__(self, info):
198
self.definition = None
199
self.body_pattern = None
200
self.checks = {}
201
self.replacement_pattern = None
202
self.matches = None
203
self.replacements = None
204
self.definition_location = None
205
206
207
class _ExtractPerformer(object):
208
209
def __init__(self, info):
210
self.info = info
211
_ExceptionalConditionChecker()(self.info)
212
213
def extract(self):
214
extract_info = self._collect_info()
215
content = codeanalyze.ChangeCollector(self.info.source)
216
definition = extract_info.definition
217
lineno, indents = extract_info.definition_location
218
offset = self.info.lines.get_line_start(lineno)
219
indented = sourceutils.fix_indentation(definition, indents)
220
content.add_change(offset, offset, indented)
221
self._replace_occurrences(content, extract_info)
222
return content.get_changed()
223
224
def _replace_occurrences(self, content, extract_info):
225
for match in extract_info.matches:
226
replacement = similarfinder.CodeTemplate(
227
extract_info.replacement_pattern)
228
mapping = {}
229
for name in replacement.get_names():
230
node = match.get_ast(name)
231
if node:
232
start, end = patchedast.node_region(match.get_ast(name))
233
mapping[name] = self.info.source[start:end]
234
else:
235
mapping[name] = name
236
region = match.get_region()
237
content.add_change(region[0], region[1],
238
replacement.substitute(mapping))
239
240
def _collect_info(self):
241
extract_collector = _ExtractCollector(self.info)
242
self._find_definition(extract_collector)
243
self._find_matches(extract_collector)
244
self._find_definition_location(extract_collector)
245
return extract_collector
246
247
def _find_matches(self, collector):
248
regions = self._where_to_search()
249
finder = similarfinder.SimilarFinder(self.info.pymodule)
250
matches = []
251
for start, end in regions:
252
matches.extend((finder.get_matches(collector.body_pattern,
253
collector.checks, start, end)))
254
collector.matches = matches
255
256
def _where_to_search(self):
257
if self.info.similar:
258
if self.info.make_global or self.info.global_:
259
return [(0, len(self.info.pymodule.source_code))]
260
if self.info.method and not self.info.variable:
261
class_scope = self.info.scope.parent
262
regions = []
263
method_kind = _get_function_kind(self.info.scope)
264
for scope in class_scope.get_scopes():
265
if method_kind == 'method' and \
266
_get_function_kind(scope) != 'method':
267
continue
268
start = self.info.lines.get_line_start(scope.get_start())
269
end = self.info.lines.get_line_end(scope.get_end())
270
regions.append((start, end))
271
return regions
272
else:
273
if self.info.variable:
274
return [self.info.scope_region]
275
else:
276
return [self.info._get_scope_region(self.info.scope.parent)]
277
else:
278
return [self.info.region]
279
280
def _find_definition_location(self, collector):
281
matched_lines = []
282
for match in collector.matches:
283
start = self.info.lines.get_line_number(match.get_region()[0])
284
start_line = self.info.logical_lines.logical_line_in(start)[0]
285
matched_lines.append(start_line)
286
location_finder = _DefinitionLocationFinder(self.info, matched_lines)
287
collector.definition_location = (location_finder.find_lineno(),
288
location_finder.find_indents())
289
290
def _find_definition(self, collector):
291
if self.info.variable:
292
parts = _ExtractVariableParts(self.info)
293
else:
294
parts = _ExtractMethodParts(self.info)
295
collector.definition = parts.get_definition()
296
collector.body_pattern = parts.get_body_pattern()
297
collector.replacement_pattern = parts.get_replacement_pattern()
298
collector.checks = parts.get_checks()
299
300
301
class _DefinitionLocationFinder(object):
302
303
def __init__(self, info, matched_lines):
304
self.info = info
305
self.matched_lines = matched_lines
306
# This only happens when subexpressions cannot be matched
307
if not matched_lines:
308
self.matched_lines.append(self.info.region_lines[0])
309
310
def find_lineno(self):
311
if self.info.variable and not self.info.make_global:
312
return self._get_before_line()
313
if self.info.make_global or self.info.global_:
314
toplevel = self._find_toplevel(self.info.scope)
315
ast = self.info.pymodule.get_ast()
316
newlines = sorted(self.matched_lines + [toplevel.get_end() + 1])
317
return suites.find_visible(ast, newlines)
318
return self._get_after_scope()
319
320
def _find_toplevel(self, scope):
321
toplevel = scope
322
if toplevel.parent is not None:
323
while toplevel.parent.parent is not None:
324
toplevel = toplevel.parent
325
return toplevel
326
327
def find_indents(self):
328
if self.info.variable and not self.info.make_global:
329
return sourceutils.get_indents(self.info.lines,
330
self._get_before_line())
331
else:
332
if self.info.global_ or self.info.make_global:
333
return 0
334
return self.info.scope_indents
335
336
def _get_before_line(self):
337
ast = self.info.scope.pyobject.get_ast()
338
return suites.find_visible(ast, self.matched_lines)
339
340
def _get_after_scope(self):
341
return self.info.scope.get_end() + 1
342
343
344
class _ExceptionalConditionChecker(object):
345
346
def __call__(self, info):
347
self.base_conditions(info)
348
if info.one_line:
349
self.one_line_conditions(info)
350
else:
351
self.multi_line_conditions(info)
352
353
def base_conditions(self, info):
354
if info.region[1] > info.scope_region[1]:
355
raise RefactoringError('Bad region selected for extract method')
356
end_line = info.region_lines[1]
357
end_scope = info.global_scope.get_inner_scope_for_line(end_line)
358
if end_scope != info.scope and end_scope.get_end() != end_line:
359
raise RefactoringError('Bad region selected for extract method')
360
try:
361
extracted = info.source[info.region[0]:info.region[1]]
362
if info.one_line:
363
extracted = '(%s)' % extracted
364
if _UnmatchedBreakOrContinueFinder.has_errors(extracted):
365
raise RefactoringError('A break/continue without having a '
366
'matching for/while loop.')
367
except SyntaxError:
368
raise RefactoringError('Extracted piece should '
369
'contain complete statements.')
370
371
def one_line_conditions(self, info):
372
if self._is_region_on_a_word(info):
373
raise RefactoringError('Should extract complete statements.')
374
if info.variable and not info.one_line:
375
raise RefactoringError('Extract variable should not '
376
'span multiple lines.')
377
378
def multi_line_conditions(self, info):
379
node = _parse_text(info.source[info.region[0]:info.region[1]])
380
count = usefunction._return_count(node)
381
if count > 1:
382
raise RefactoringError('Extracted piece can have only one '
383
'return statement.')
384
if usefunction._yield_count(node):
385
raise RefactoringError('Extracted piece cannot '
386
'have yield statements.')
387
if count == 1 and not usefunction._returns_last(node):
388
raise RefactoringError('Return should be the last statement.')
389
if info.region != info.lines_region:
390
raise RefactoringError('Extracted piece should '
391
'contain complete statements.')
392
393
def _is_region_on_a_word(self, info):
394
if info.region[0] > 0 and self._is_on_a_word(info, info.region[0] - 1) or \
395
self._is_on_a_word(info, info.region[1] - 1):
396
return True
397
398
def _is_on_a_word(self, info, offset):
399
prev = info.source[offset]
400
if not (prev.isalnum() or prev == '_') or \
401
offset + 1 == len(info.source):
402
return False
403
next = info.source[offset + 1]
404
return next.isalnum() or next == '_'
405
406
407
class _ExtractMethodParts(object):
408
409
def __init__(self, info):
410
self.info = info
411
self.info_collector = self._create_info_collector()
412
413
def get_definition(self):
414
if self.info.global_:
415
return '\n%s\n' % self._get_function_definition()
416
else:
417
return '\n%s' % self._get_function_definition()
418
419
def get_replacement_pattern(self):
420
variables = []
421
variables.extend(self._find_function_arguments())
422
variables.extend(self._find_function_returns())
423
return similarfinder.make_pattern(self._get_call(), variables)
424
425
def get_body_pattern(self):
426
variables = []
427
variables.extend(self._find_function_arguments())
428
variables.extend(self._find_function_returns())
429
variables.extend(self._find_temps())
430
return similarfinder.make_pattern(self._get_body(), variables)
431
432
def _get_body(self):
433
result = sourceutils.fix_indentation(self.info.extracted, 0)
434
if self.info.one_line:
435
result = '(%s)' % result
436
return result
437
438
def _find_temps(self):
439
return usefunction.find_temps(self.info.pycore.project,
440
self._get_body())
441
442
def get_checks(self):
443
if self.info.method and not self.info.make_global:
444
if _get_function_kind(self.info.scope) == 'method':
445
class_name = similarfinder._pydefined_to_str(
446
self.info.scope.parent.pyobject)
447
return {self._get_self_name(): 'type=' + class_name}
448
return {}
449
450
def _create_info_collector(self):
451
zero = self.info.scope.get_start() - 1
452
start_line = self.info.region_lines[0] - zero
453
end_line = self.info.region_lines[1] - zero
454
info_collector = _FunctionInformationCollector(start_line, end_line,
455
self.info.global_)
456
body = self.info.source[self.info.scope_region[0]:
457
self.info.scope_region[1]]
458
node = _parse_text(body)
459
ast.walk(node, info_collector)
460
return info_collector
461
462
def _get_function_definition(self):
463
args = self._find_function_arguments()
464
returns = self._find_function_returns()
465
result = []
466
if self.info.method and not self.info.make_global and \
467
_get_function_kind(self.info.scope) != 'method':
468
result.append('@staticmethod\n')
469
result.append('def %s:\n' % self._get_function_signature(args))
470
unindented_body = self._get_unindented_function_body(returns)
471
indents = sourceutils.get_indent(self.info.pycore)
472
function_body = sourceutils.indent_lines(unindented_body, indents)
473
result.append(function_body)
474
definition = ''.join(result)
475
476
return definition + '\n'
477
478
def _get_function_signature(self, args):
479
args = list(args)
480
prefix = ''
481
if self._extracting_method():
482
self_name = self._get_self_name()
483
if self_name is None:
484
raise RefactoringError('Extracting a method from a function '
485
'with no self argument.')
486
if self_name in args:
487
args.remove(self_name)
488
args.insert(0, self_name)
489
return prefix + self.info.new_name + \
490
'(%s)' % self._get_comma_form(args)
491
492
def _extracting_method(self):
493
return self.info.method and not self.info.make_global and \
494
_get_function_kind(self.info.scope) == 'method'
495
496
def _get_self_name(self):
497
param_names = self.info.scope.pyobject.get_param_names()
498
if param_names:
499
return param_names[0]
500
501
def _get_function_call(self, args):
502
prefix = ''
503
if self.info.method and not self.info.make_global:
504
if _get_function_kind(self.info.scope) == 'method':
505
self_name = self._get_self_name()
506
if self_name in args:
507
args.remove(self_name)
508
prefix = self_name + '.'
509
else:
510
prefix = self.info.scope.parent.pyobject.get_name() + '.'
511
return prefix + '%s(%s)' % (self.info.new_name,
512
self._get_comma_form(args))
513
514
def _get_comma_form(self, names):
515
result = ''
516
if names:
517
result += names[0]
518
for name in names[1:]:
519
result += ', ' + name
520
return result
521
522
def _get_call(self):
523
if self.info.one_line:
524
args = self._find_function_arguments()
525
return self._get_function_call(args)
526
args = self._find_function_arguments()
527
returns = self._find_function_returns()
528
call_prefix = ''
529
if returns:
530
call_prefix = self._get_comma_form(returns) + ' = '
531
if self.info.returned:
532
call_prefix = 'return '
533
return call_prefix + self._get_function_call(args)
534
535
def _find_function_arguments(self):
536
# if not make_global, do not pass any global names; they are
537
# all visible.
538
if self.info.global_ and not self.info.make_global:
539
return ()
540
if not self.info.one_line:
541
result = (self.info_collector.prewritten &
542
self.info_collector.read)
543
result |= (self.info_collector.prewritten &
544
self.info_collector.postread &
545
(self.info_collector.maybe_written -
546
self.info_collector.written))
547
return list(result)
548
start = self.info.region[0]
549
if start == self.info.lines_region[0]:
550
start = start + re.search('\S', self.info.extracted).start()
551
function_definition = self.info.source[start:self.info.region[1]]
552
read = _VariableReadsAndWritesFinder.find_reads_for_one_liners(
553
function_definition)
554
return list(self.info_collector.prewritten.intersection(read))
555
556
def _find_function_returns(self):
557
if self.info.one_line or self.info.returned:
558
return []
559
written = self.info_collector.written | \
560
self.info_collector.maybe_written
561
return list(written & self.info_collector.postread)
562
563
def _get_unindented_function_body(self, returns):
564
if self.info.one_line:
565
return 'return ' + _join_lines(self.info.extracted)
566
extracted_body = self.info.extracted
567
unindented_body = sourceutils.fix_indentation(extracted_body, 0)
568
if returns:
569
unindented_body += '\nreturn %s' % self._get_comma_form(returns)
570
return unindented_body
571
572
573
class _ExtractVariableParts(object):
574
575
def __init__(self, info):
576
self.info = info
577
578
def get_definition(self):
579
result = self.info.new_name + ' = ' + \
580
_join_lines(self.info.extracted) + '\n'
581
return result
582
583
def get_body_pattern(self):
584
return '(%s)' % self.info.extracted.strip()
585
586
def get_replacement_pattern(self):
587
return self.info.new_name
588
589
def get_checks(self):
590
return {}
591
592
593
class _FunctionInformationCollector(object):
594
595
def __init__(self, start, end, is_global):
596
self.start = start
597
self.end = end
598
self.is_global = is_global
599
self.prewritten = set()
600
self.maybe_written = set()
601
self.written = set()
602
self.read = set()
603
self.postread = set()
604
self.postwritten = set()
605
self.host_function = True
606
self.conditional = False
607
608
def _read_variable(self, name, lineno):
609
if self.start <= lineno <= self.end:
610
if name not in self.written:
611
self.read.add(name)
612
if self.end < lineno:
613
if name not in self.postwritten:
614
self.postread.add(name)
615
616
def _written_variable(self, name, lineno):
617
if self.start <= lineno <= self.end:
618
if self.conditional:
619
self.maybe_written.add(name)
620
else:
621
self.written.add(name)
622
if self.start > lineno:
623
self.prewritten.add(name)
624
if self.end < lineno:
625
self.postwritten.add(name)
626
627
def _FunctionDef(self, node):
628
if not self.is_global and self.host_function:
629
self.host_function = False
630
for name in _get_argnames(node.args):
631
self._written_variable(name, node.lineno)
632
for child in node.body:
633
ast.walk(child, self)
634
else:
635
self._written_variable(node.name, node.lineno)
636
visitor = _VariableReadsAndWritesFinder()
637
for child in node.body:
638
ast.walk(child, visitor)
639
for name in visitor.read - visitor.written:
640
self._read_variable(name, node.lineno)
641
642
def _Name(self, node):
643
if isinstance(node.ctx, (ast.Store, ast.AugStore)):
644
self._written_variable(node.id, node.lineno)
645
if not isinstance(node.ctx, ast.Store):
646
self._read_variable(node.id, node.lineno)
647
648
def _Assign(self, node):
649
ast.walk(node.value, self)
650
for child in node.targets:
651
ast.walk(child, self)
652
653
def _ClassDef(self, node):
654
self._written_variable(node.name, node.lineno)
655
656
def _handle_conditional_node(self, node):
657
self.conditional = True
658
try:
659
for child in ast.get_child_nodes(node):
660
ast.walk(child, self)
661
finally:
662
self.conditional = False
663
664
def _If(self, node):
665
self._handle_conditional_node(node)
666
667
def _While(self, node):
668
self._handle_conditional_node(node)
669
670
def _For(self, node):
671
self._handle_conditional_node(node)
672
673
674
675
def _get_argnames(arguments):
676
result = [node.id for node in arguments.args
677
if isinstance(node, ast.Name)]
678
if arguments.vararg:
679
result.append(arguments.vararg)
680
if arguments.kwarg:
681
result.append(arguments.kwarg)
682
return result
683
684
685
class _VariableReadsAndWritesFinder(object):
686
687
def __init__(self):
688
self.written = set()
689
self.read = set()
690
691
def _Name(self, node):
692
if isinstance(node.ctx, (ast.Store, ast.AugStore)):
693
self.written.add(node.id)
694
if not isinstance(node, ast.Store):
695
self.read.add(node.id)
696
697
def _FunctionDef(self, node):
698
self.written.add(node.name)
699
visitor = _VariableReadsAndWritesFinder()
700
for child in ast.get_child_nodes(node):
701
ast.walk(child, visitor)
702
self.read.update(visitor.read - visitor.written)
703
704
def _Class(self, node):
705
self.written.add(node.name)
706
707
@staticmethod
708
def find_reads_and_writes(code):
709
if code.strip() == '':
710
return set(), set()
711
if isinstance(code, unicode):
712
code = code.encode('utf-8')
713
node = _parse_text(code)
714
visitor = _VariableReadsAndWritesFinder()
715
ast.walk(node, visitor)
716
return visitor.read, visitor.written
717
718
@staticmethod
719
def find_reads_for_one_liners(code):
720
if code.strip() == '':
721
return set(), set()
722
node = _parse_text(code)
723
visitor = _VariableReadsAndWritesFinder()
724
ast.walk(node, visitor)
725
return visitor.read
726
727
728
class _UnmatchedBreakOrContinueFinder(object):
729
730
def __init__(self):
731
self.error = False
732
self.loop_count = 0
733
734
def _For(self, node):
735
self.loop_encountered(node)
736
737
def _While(self, node):
738
self.loop_encountered(node)
739
740
def loop_encountered(self, node):
741
self.loop_count += 1
742
for child in node.body:
743
ast.walk(child, self)
744
self.loop_count -= 1
745
if node.orelse:
746
ast.walk(node.orelse, self)
747
748
def _Break(self, node):
749
self.check_loop()
750
751
def _Continue(self, node):
752
self.check_loop()
753
754
def check_loop(self):
755
if self.loop_count < 1:
756
self.error = True
757
758
def _FunctionDef(self, node):
759
pass
760
761
def _ClassDef(self, node):
762
pass
763
764
@staticmethod
765
def has_errors(code):
766
if code.strip() == '':
767
return False
768
node = _parse_text(code)
769
visitor = _UnmatchedBreakOrContinueFinder()
770
ast.walk(node, visitor)
771
return visitor.error
772
773
def _get_function_kind(scope):
774
return scope.pyobject.get_kind()
775
776
777
def _parse_text(body):
778
body = sourceutils.fix_indentation(body, 0)
779
node = ast.parse(body)
780
return node
781
782
def _join_lines(code):
783
lines = []
784
for line in code.splitlines():
785
if line.endswith('\\'):
786
lines.append(line[:-1].strip())
787
else:
788
lines.append(line.strip())
789
return ' '.join(lines)
790
791