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/inline.py
1415 views
1
# Known Bugs when inlining a function/method
2
# The values passed to function are inlined using _inlined_variable.
3
# This may cause two problems, illustrated in the examples below
4
#
5
# def foo(var1):
6
# var1 = var1*10
7
# return var1
8
#
9
# If a call to foo(20) is inlined, the result of inlined function is 20,
10
# but it should be 200.
11
#
12
# def foo(var1):
13
# var2 = var1*10
14
# return var2
15
#
16
# 2- If a call to foo(10+10) is inlined the result of inlined function is 110
17
# but it should be 200.
18
19
import re
20
21
import rope.base.exceptions
22
import rope.refactor.functionutils
23
from rope.base import (pynames, pyobjects, codeanalyze,
24
taskhandle, evaluate, worder, utils)
25
from rope.base.change import ChangeSet, ChangeContents
26
from rope.refactor import (occurrences, rename, sourceutils,
27
importutils, move, change_signature)
28
29
def unique_prefix():
30
n = 0
31
while True:
32
yield "__" + str(n) + "__"
33
n += 1
34
35
def create_inline(project, resource, offset):
36
"""Create a refactoring object for inlining
37
38
Based on `resource` and `offset` it returns an instance of
39
`InlineMethod`, `InlineVariable` or `InlineParameter`.
40
41
"""
42
pycore = project.pycore
43
pyname = _get_pyname(pycore, resource, offset)
44
message = 'Inline refactoring should be performed on ' \
45
'a method, local variable or parameter.'
46
if pyname is None:
47
raise rope.base.exceptions.RefactoringError(message)
48
if isinstance(pyname, pynames.ImportedName):
49
pyname = pyname._get_imported_pyname()
50
if isinstance(pyname, pynames.AssignedName):
51
return InlineVariable(project, resource, offset)
52
if isinstance(pyname, pynames.ParameterName):
53
return InlineParameter(project, resource, offset)
54
if isinstance(pyname.get_object(), pyobjects.PyFunction):
55
return InlineMethod(project, resource, offset)
56
else:
57
raise rope.base.exceptions.RefactoringError(message)
58
59
60
class _Inliner(object):
61
62
def __init__(self, project, resource, offset):
63
self.project = project
64
self.pycore = project.pycore
65
self.pyname = _get_pyname(self.pycore, resource, offset)
66
range_finder = worder.Worder(resource.read())
67
self.region = range_finder.get_primary_range(offset)
68
self.name = range_finder.get_word_at(offset)
69
self.offset = offset
70
self.original = resource
71
72
def get_changes(self, *args, **kwds):
73
pass
74
75
def get_kind(self):
76
"""Return either 'variable', 'method' or 'parameter'"""
77
78
79
class InlineMethod(_Inliner):
80
81
def __init__(self, *args, **kwds):
82
super(InlineMethod, self).__init__(*args, **kwds)
83
self.pyfunction = self.pyname.get_object()
84
self.pymodule = self.pyfunction.get_module()
85
self.resource = self.pyfunction.get_module().get_resource()
86
self.occurrence_finder = occurrences.create_finder(
87
self.pycore, self.name, self.pyname)
88
self.normal_generator = _DefinitionGenerator(self.project,
89
self.pyfunction)
90
self._init_imports()
91
92
def _init_imports(self):
93
body = sourceutils.get_body(self.pyfunction)
94
body, imports = move.moving_code_with_imports(
95
self.pycore, self.resource, body)
96
self.imports = imports
97
self.others_generator = _DefinitionGenerator(
98
self.project, self.pyfunction, body=body)
99
100
def _get_scope_range(self):
101
scope = self.pyfunction.get_scope()
102
lines = self.pymodule.lines
103
logicals = self.pymodule.logical_lines
104
start_line = scope.get_start()
105
if self.pyfunction.decorators:
106
decorators = self.pyfunction.decorators
107
if hasattr(decorators[0], 'lineno'):
108
start_line = decorators[0].lineno
109
start_offset = lines.get_line_start(start_line)
110
end_offset = min(lines.get_line_end(scope.end) + 1,
111
len(self.pymodule.source_code))
112
return (start_offset, end_offset)
113
114
def get_changes(self, remove=True, only_current=False, resources=None,
115
task_handle=taskhandle.NullTaskHandle()):
116
"""Get the changes this refactoring makes
117
118
If `remove` is `False` the definition will not be removed. If
119
`only_current` is `True`, the the current occurrence will be
120
inlined, only.
121
"""
122
changes = ChangeSet('Inline method <%s>' % self.name)
123
if resources is None:
124
resources = self.pycore.get_python_files()
125
if only_current:
126
resources = [self.original]
127
if remove:
128
resources.append(self.resource)
129
job_set = task_handle.create_jobset('Collecting Changes',
130
len(resources))
131
for file in resources:
132
job_set.started_job(file.path)
133
if file == self.resource:
134
changes.add_change(self._defining_file_changes(
135
changes, remove=remove, only_current=only_current))
136
else:
137
aim = None
138
if only_current and self.original == file:
139
aim = self.offset
140
handle = _InlineFunctionCallsForModuleHandle(
141
self.pycore, file, self.others_generator, aim)
142
result = move.ModuleSkipRenamer(
143
self.occurrence_finder, file, handle).get_changed_module()
144
if result is not None:
145
result = _add_imports(self.pycore, result,
146
file, self.imports)
147
if remove:
148
result = _remove_from(self.pycore, self.pyname,
149
result, file)
150
changes.add_change(ChangeContents(file, result))
151
job_set.finished_job()
152
return changes
153
154
def _get_removed_range(self):
155
scope = self.pyfunction.get_scope()
156
lines = self.pymodule.lines
157
logical = self.pymodule.logical_lines
158
start_line = scope.get_start()
159
start, end = self._get_scope_range()
160
end_line = scope.get_end()
161
for i in range(end_line + 1, lines.length()):
162
if lines.get_line(i).strip() == '':
163
end_line = i
164
else:
165
break
166
end = min(lines.get_line_end(end_line) + 1,
167
len(self.pymodule.source_code))
168
return (start, end)
169
170
def _defining_file_changes(self, changes, remove, only_current):
171
start_offset, end_offset = self._get_removed_range()
172
aim = None
173
if only_current:
174
if self.resource == self.original:
175
aim = self.offset
176
else:
177
# we don't want to change any of them
178
aim = len(self.resource.read()) + 100
179
handle = _InlineFunctionCallsForModuleHandle(
180
self.pycore, self.resource,
181
self.normal_generator, aim_offset=aim)
182
replacement = None
183
if remove:
184
replacement = self._get_method_replacement()
185
result = move.ModuleSkipRenamer(
186
self.occurrence_finder, self.resource, handle, start_offset,
187
end_offset, replacement).get_changed_module()
188
return ChangeContents(self.resource, result)
189
190
def _get_method_replacement(self):
191
if self._is_the_last_method_of_a_class():
192
indents = sourceutils.get_indents(
193
self.pymodule.lines, self.pyfunction.get_scope().get_start())
194
return ' ' * indents + 'pass\n'
195
return ''
196
197
def _is_the_last_method_of_a_class(self):
198
pyclass = self.pyfunction.parent
199
if not isinstance(pyclass, pyobjects.PyClass):
200
return False
201
class_start, class_end = sourceutils.get_body_region(pyclass)
202
source = self.pymodule.source_code
203
lines = self.pymodule.lines
204
func_start, func_end = self._get_scope_range()
205
if source[class_start:func_start].strip() == '' and \
206
source[func_end:class_end].strip() == '':
207
return True
208
return False
209
210
def get_kind(self):
211
return 'method'
212
213
214
class InlineVariable(_Inliner):
215
216
def __init__(self, *args, **kwds):
217
super(InlineVariable, self).__init__(*args, **kwds)
218
self.pymodule = self.pyname.get_definition_location()[0]
219
self.resource = self.pymodule.get_resource()
220
self._check_exceptional_conditions()
221
self._init_imports()
222
223
def _check_exceptional_conditions(self):
224
if len(self.pyname.assignments) != 1:
225
raise rope.base.exceptions.RefactoringError(
226
'Local variable should be assigned once for inlining.')
227
228
def get_changes(self, remove=True, only_current=False, resources=None,
229
task_handle=taskhandle.NullTaskHandle()):
230
if resources is None:
231
if rename._is_local(self.pyname):
232
resources = [self.resource]
233
else:
234
resources = self.pycore.get_python_files()
235
if only_current:
236
resources = [self.original]
237
if remove and self.original != self.resource:
238
resources.append(self.resource)
239
changes = ChangeSet('Inline variable <%s>' % self.name)
240
jobset = task_handle.create_jobset('Calculating changes',
241
len(resources))
242
243
for resource in resources:
244
jobset.started_job(resource.path)
245
if resource == self.resource:
246
source = self._change_main_module(remove, only_current)
247
changes.add_change(ChangeContents(self.resource, source))
248
else:
249
result = self._change_module(resource, remove, only_current)
250
if result is not None:
251
result = _add_imports(self.pycore, result,
252
resource, self.imports)
253
changes.add_change(ChangeContents(resource, result))
254
jobset.finished_job()
255
return changes
256
257
def _change_main_module(self, remove, only_current):
258
region = None
259
if only_current and self.original == self.resource:
260
region = self.region
261
return _inline_variable(self.pycore, self.pymodule, self.pyname,
262
self.name, remove=remove, region=region)
263
264
def _init_imports(self):
265
vardef = _getvardef(self.pymodule, self.pyname)
266
self.imported, self.imports = move.moving_code_with_imports(
267
self.pycore, self.resource, vardef)
268
269
def _change_module(self, resource, remove, only_current):
270
filters = [occurrences.NoImportsFilter(),
271
occurrences.PyNameFilter(self.pyname)]
272
if only_current and resource == self.original:
273
def check_aim(occurrence):
274
start, end = occurrence.get_primary_range()
275
if self.offset < start or end < self.offset:
276
return False
277
filters.insert(0, check_aim)
278
finder = occurrences.Finder(self.pycore, self.name, filters=filters)
279
changed = rename.rename_in_module(
280
finder, self.imported, resource=resource, replace_primary=True)
281
if changed and remove:
282
changed = _remove_from(self.pycore, self.pyname, changed, resource)
283
return changed
284
285
def get_kind(self):
286
return 'variable'
287
288
289
class InlineParameter(_Inliner):
290
291
def __init__(self, *args, **kwds):
292
super(InlineParameter, self).__init__(*args, **kwds)
293
resource, offset = self._function_location()
294
index = self.pyname.index
295
self.changers = [change_signature.ArgumentDefaultInliner(index)]
296
self.signature = change_signature.ChangeSignature(self.project,
297
resource, offset)
298
299
def _function_location(self):
300
pymodule, lineno = self.pyname.get_definition_location()
301
resource = pymodule.get_resource()
302
start = pymodule.lines.get_line_start(lineno)
303
word_finder = worder.Worder(pymodule.source_code)
304
offset = word_finder.find_function_offset(start)
305
return resource, offset
306
307
def get_changes(self, **kwds):
308
"""Get the changes needed by this refactoring
309
310
See `rope.refactor.change_signature.ChangeSignature.get_changes()`
311
for arguments.
312
"""
313
return self.signature.get_changes(self.changers, **kwds)
314
315
def get_kind(self):
316
return 'parameter'
317
318
319
def _join_lines(lines):
320
definition_lines = []
321
for unchanged_line in lines:
322
line = unchanged_line.strip()
323
if line.endswith('\\'):
324
line = line[:-1].strip()
325
definition_lines.append(line)
326
joined = ' '.join(definition_lines)
327
return joined
328
329
330
class _DefinitionGenerator(object):
331
unique_prefix = unique_prefix()
332
def __init__(self, project, pyfunction, body=None):
333
self.pycore = project.pycore
334
self.pyfunction = pyfunction
335
self.pymodule = pyfunction.get_module()
336
self.resource = self.pymodule.get_resource()
337
self.definition_info = self._get_definition_info()
338
self.definition_params = self._get_definition_params()
339
self._calculated_definitions = {}
340
if body is not None:
341
self.body = body
342
else:
343
self.body = sourceutils.get_body(self.pyfunction)
344
345
def _get_definition_info(self):
346
return rope.refactor.functionutils.DefinitionInfo.read(self.pyfunction)
347
348
def _get_definition_params(self):
349
definition_info = self.definition_info
350
paramdict = dict([pair for pair in definition_info.args_with_defaults])
351
if definition_info.args_arg is not None or \
352
definition_info.keywords_arg is not None:
353
raise rope.base.exceptions.RefactoringError(
354
'Cannot inline functions with list and keyword arguements.')
355
if self.pyfunction.get_kind() == 'classmethod':
356
paramdict[definition_info.args_with_defaults[0][0]] = \
357
self.pyfunction.parent.get_name()
358
return paramdict
359
360
def get_function_name(self):
361
return self.pyfunction.get_name()
362
363
def get_definition(self, primary, pyname, call, host_vars=[],returns=False):
364
# caching already calculated definitions
365
return self._calculate_definition(primary, pyname, call,
366
host_vars, returns)
367
368
def _calculate_header(self, primary, pyname, call):
369
# A header is created which initializes parameters
370
# to the values passed to the function.
371
call_info = rope.refactor.functionutils.CallInfo.read(
372
primary, pyname, self.definition_info, call)
373
paramdict = self.definition_params
374
mapping = rope.refactor.functionutils.ArgumentMapping(
375
self.definition_info, call_info)
376
for param_name, value in mapping.param_dict.items():
377
paramdict[param_name] = value
378
header = ''
379
to_be_inlined = []
380
mod = self.pycore.get_string_module(self.body)
381
all_names = mod.get_scope().get_names()
382
assigned_names = [name for name in all_names if
383
isinstance(all_names[name], rope.base.pynamesdef.AssignedName)]
384
for name, value in paramdict.items():
385
if name != value and value is not None:
386
header += name + ' = ' + value.replace('\n', ' ') + '\n'
387
to_be_inlined.append(name)
388
return header, to_be_inlined
389
390
def _calculate_definition(self, primary, pyname, call, host_vars, returns):
391
392
header, to_be_inlined = self._calculate_header(primary, pyname, call)
393
394
source = header + self.body
395
mod = self.pycore.get_string_module(source)
396
name_dict = mod.get_scope().get_names()
397
all_names = [x for x in name_dict if
398
not isinstance(name_dict[x], rope.base.builtins.BuiltinName)]
399
400
# If there is a name conflict, all variable names
401
# inside the inlined function are renamed
402
if len(set(all_names).intersection(set(host_vars))) > 0:
403
404
prefix = _DefinitionGenerator.unique_prefix.next()
405
guest = self.pycore.get_string_module(source, self.resource)
406
407
to_be_inlined = [prefix+item for item in to_be_inlined]
408
for item in all_names:
409
pyname = guest[item]
410
occurrence_finder = occurrences.create_finder(
411
self.pycore, item, pyname)
412
source = rename.rename_in_module(occurrence_finder,
413
prefix+item, pymodule=guest)
414
guest = self.pycore.get_string_module(source, self.resource)
415
416
#parameters not reassigned inside the functions are now inlined.
417
for name in to_be_inlined:
418
pymodule = self.pycore.get_string_module(source, self.resource)
419
pyname = pymodule[name]
420
source = _inline_variable(self.pycore, pymodule, pyname, name)
421
422
return self._replace_returns_with(source, returns)
423
424
def _replace_returns_with(self, source, returns):
425
result = []
426
returned = None
427
last_changed = 0
428
for match in _DefinitionGenerator._get_return_pattern().finditer(source):
429
for key, value in match.groupdict().items():
430
if value and key == 'return':
431
result.append(source[last_changed:match.start('return')])
432
if returns:
433
self._check_nothing_after_return(source,
434
match.end('return'))
435
returned = _join_lines(
436
source[match.end('return'): len(source)].splitlines())
437
last_changed = len(source)
438
else:
439
current = match.end('return')
440
while current < len(source) and source[current] in ' \t':
441
current += 1
442
last_changed = current
443
if current == len(source) or source[current] == '\n':
444
result.append('pass')
445
result.append(source[last_changed:])
446
return ''.join(result), returned
447
448
def _check_nothing_after_return(self, source, offset):
449
lines = codeanalyze.SourceLinesAdapter(source)
450
lineno = lines.get_line_number(offset)
451
logical_lines = codeanalyze.LogicalLineFinder(lines)
452
lineno = logical_lines.logical_line_in(lineno)[1]
453
if source[lines.get_line_end(lineno):len(source)].strip() != '':
454
raise rope.base.exceptions.RefactoringError(
455
'Cannot inline functions with statements after return statement.')
456
457
@classmethod
458
def _get_return_pattern(cls):
459
if not hasattr(cls, '_return_pattern'):
460
def named_pattern(name, list_):
461
return "(?P<%s>" % name + "|".join(list_) + ")"
462
comment_pattern = named_pattern('comment', [r'#[^\n]*'])
463
string_pattern = named_pattern('string',
464
[codeanalyze.get_string_pattern()])
465
return_pattern = r'\b(?P<return>return)\b'
466
cls._return_pattern = re.compile(comment_pattern + "|" +
467
string_pattern + "|" +
468
return_pattern)
469
return cls._return_pattern
470
471
472
class _InlineFunctionCallsForModuleHandle(object):
473
474
def __init__(self, pycore, resource,
475
definition_generator, aim_offset=None):
476
"""Inlines occurrences
477
478
If `aim` is not `None` only the occurrences that intersect
479
`aim` offset will be inlined.
480
481
"""
482
self.pycore = pycore
483
self.generator = definition_generator
484
self.resource = resource
485
self.aim = aim_offset
486
487
def occurred_inside_skip(self, change_collector, occurrence):
488
if not occurrence.is_defined():
489
raise rope.base.exceptions.RefactoringError(
490
'Cannot inline functions that reference themselves')
491
492
def occurred_outside_skip(self, change_collector, occurrence):
493
start, end = occurrence.get_primary_range()
494
# we remove out of date imports later
495
if occurrence.is_in_import_statement():
496
return
497
# the function is referenced outside an import statement
498
if not occurrence.is_called():
499
raise rope.base.exceptions.RefactoringError(
500
'Reference to inlining function other than function call'
501
' in <file: %s, offset: %d>' % (self.resource.path, start))
502
if self.aim is not None and (self.aim < start or self.aim > end):
503
return
504
end_parens = self._find_end_parens(self.source, end - 1)
505
lineno = self.lines.get_line_number(start)
506
start_line, end_line = self.pymodule.logical_lines.\
507
logical_line_in(lineno)
508
line_start = self.lines.get_line_start(start_line)
509
line_end = self.lines.get_line_end(end_line)
510
511
512
returns = self.source[line_start:start].strip() != '' or \
513
self.source[end_parens:line_end].strip() != ''
514
indents = sourceutils.get_indents(self.lines, start_line)
515
primary, pyname = occurrence.get_primary_and_pyname()
516
517
host = self.pycore.resource_to_pyobject(self.resource)
518
scope = host.scope.get_inner_scope_for_line(lineno)
519
definition, returned = self.generator.get_definition(
520
primary, pyname, self.source[start:end_parens], scope.get_names(), returns=returns)
521
522
end = min(line_end + 1, len(self.source))
523
change_collector.add_change(line_start, end,
524
sourceutils.fix_indentation(definition, indents))
525
if returns:
526
name = returned
527
if name is None:
528
name = 'None'
529
change_collector.add_change(
530
line_end, end, self.source[line_start:start] + name +
531
self.source[end_parens:end])
532
533
def _find_end_parens(self, source, offset):
534
finder = worder.Worder(source)
535
return finder.get_word_parens_range(offset)[1]
536
537
@property
538
@utils.saveit
539
def pymodule(self):
540
return self.pycore.resource_to_pyobject(self.resource)
541
542
@property
543
@utils.saveit
544
def source(self):
545
if self.resource is not None:
546
return self.resource.read()
547
else:
548
return self.pymodule.source_code
549
550
@property
551
@utils.saveit
552
def lines(self):
553
return self.pymodule.lines
554
555
556
def _inline_variable(pycore, pymodule, pyname, name,
557
remove=True, region=None):
558
definition = _getvardef(pymodule, pyname)
559
start, end = _assigned_lineno(pymodule, pyname)
560
561
occurrence_finder = occurrences.create_finder(pycore, name, pyname)
562
changed_source = rename.rename_in_module(
563
occurrence_finder, definition, pymodule=pymodule,
564
replace_primary=True, writes=False, region=region)
565
if changed_source is None:
566
changed_source = pymodule.source_code
567
if remove:
568
lines = codeanalyze.SourceLinesAdapter(changed_source)
569
source = changed_source[:lines.get_line_start(start)] + \
570
changed_source[lines.get_line_end(end) + 1:]
571
else:
572
source = changed_source
573
return source
574
575
def _getvardef(pymodule, pyname):
576
assignment = pyname.assignments[0]
577
lines = pymodule.lines
578
start, end = _assigned_lineno(pymodule, pyname)
579
definition_with_assignment = _join_lines(
580
[lines.get_line(n) for n in range(start, end + 1)])
581
if assignment.levels:
582
raise rope.base.exceptions.RefactoringError(
583
'Cannot inline tuple assignments.')
584
definition = definition_with_assignment[definition_with_assignment.\
585
index('=') + 1:].strip()
586
return definition
587
588
def _assigned_lineno(pymodule, pyname):
589
definition_line = pyname.assignments[0].ast_node.lineno
590
return pymodule.logical_lines.logical_line_in(definition_line)
591
592
def _add_imports(pycore, source, resource, imports):
593
if not imports:
594
return source
595
pymodule = pycore.get_string_module(source, resource)
596
module_import = importutils.get_module_imports(pycore, pymodule)
597
for import_info in imports:
598
module_import.add_import(import_info)
599
source = module_import.get_changed_source()
600
pymodule = pycore.get_string_module(source, resource)
601
import_tools = importutils.ImportTools(pycore)
602
return import_tools.organize_imports(pymodule, unused=False, sort=False)
603
604
def _get_pyname(pycore, resource, offset):
605
pymodule = pycore.resource_to_pyobject(resource)
606
pyname = evaluate.eval_location(pymodule, offset)
607
if isinstance(pyname, pynames.ImportedName):
608
pyname = pyname._get_imported_pyname()
609
return pyname
610
611
def _remove_from(pycore, pyname, source, resource):
612
pymodule = pycore.get_string_module(source, resource)
613
module_import = importutils.get_module_imports(pycore, pymodule)
614
module_import.remove_pyname(pyname)
615
return module_import.get_changed_source()
616
617