Path: blob/master/elisp/emacs-for-python/rope-dist/rope/refactor/inline.py
1415 views
# Known Bugs when inlining a function/method1# The values passed to function are inlined using _inlined_variable.2# This may cause two problems, illustrated in the examples below3#4# def foo(var1):5# var1 = var1*106# return var17#8# If a call to foo(20) is inlined, the result of inlined function is 20,9# but it should be 200.10#11# def foo(var1):12# var2 = var1*1013# return var214#15# 2- If a call to foo(10+10) is inlined the result of inlined function is 11016# but it should be 200.1718import re1920import rope.base.exceptions21import rope.refactor.functionutils22from rope.base import (pynames, pyobjects, codeanalyze,23taskhandle, evaluate, worder, utils)24from rope.base.change import ChangeSet, ChangeContents25from rope.refactor import (occurrences, rename, sourceutils,26importutils, move, change_signature)2728def unique_prefix():29n = 030while True:31yield "__" + str(n) + "__"32n += 13334def create_inline(project, resource, offset):35"""Create a refactoring object for inlining3637Based on `resource` and `offset` it returns an instance of38`InlineMethod`, `InlineVariable` or `InlineParameter`.3940"""41pycore = project.pycore42pyname = _get_pyname(pycore, resource, offset)43message = 'Inline refactoring should be performed on ' \44'a method, local variable or parameter.'45if pyname is None:46raise rope.base.exceptions.RefactoringError(message)47if isinstance(pyname, pynames.ImportedName):48pyname = pyname._get_imported_pyname()49if isinstance(pyname, pynames.AssignedName):50return InlineVariable(project, resource, offset)51if isinstance(pyname, pynames.ParameterName):52return InlineParameter(project, resource, offset)53if isinstance(pyname.get_object(), pyobjects.PyFunction):54return InlineMethod(project, resource, offset)55else:56raise rope.base.exceptions.RefactoringError(message)575859class _Inliner(object):6061def __init__(self, project, resource, offset):62self.project = project63self.pycore = project.pycore64self.pyname = _get_pyname(self.pycore, resource, offset)65range_finder = worder.Worder(resource.read())66self.region = range_finder.get_primary_range(offset)67self.name = range_finder.get_word_at(offset)68self.offset = offset69self.original = resource7071def get_changes(self, *args, **kwds):72pass7374def get_kind(self):75"""Return either 'variable', 'method' or 'parameter'"""767778class InlineMethod(_Inliner):7980def __init__(self, *args, **kwds):81super(InlineMethod, self).__init__(*args, **kwds)82self.pyfunction = self.pyname.get_object()83self.pymodule = self.pyfunction.get_module()84self.resource = self.pyfunction.get_module().get_resource()85self.occurrence_finder = occurrences.create_finder(86self.pycore, self.name, self.pyname)87self.normal_generator = _DefinitionGenerator(self.project,88self.pyfunction)89self._init_imports()9091def _init_imports(self):92body = sourceutils.get_body(self.pyfunction)93body, imports = move.moving_code_with_imports(94self.pycore, self.resource, body)95self.imports = imports96self.others_generator = _DefinitionGenerator(97self.project, self.pyfunction, body=body)9899def _get_scope_range(self):100scope = self.pyfunction.get_scope()101lines = self.pymodule.lines102logicals = self.pymodule.logical_lines103start_line = scope.get_start()104if self.pyfunction.decorators:105decorators = self.pyfunction.decorators106if hasattr(decorators[0], 'lineno'):107start_line = decorators[0].lineno108start_offset = lines.get_line_start(start_line)109end_offset = min(lines.get_line_end(scope.end) + 1,110len(self.pymodule.source_code))111return (start_offset, end_offset)112113def get_changes(self, remove=True, only_current=False, resources=None,114task_handle=taskhandle.NullTaskHandle()):115"""Get the changes this refactoring makes116117If `remove` is `False` the definition will not be removed. If118`only_current` is `True`, the the current occurrence will be119inlined, only.120"""121changes = ChangeSet('Inline method <%s>' % self.name)122if resources is None:123resources = self.pycore.get_python_files()124if only_current:125resources = [self.original]126if remove:127resources.append(self.resource)128job_set = task_handle.create_jobset('Collecting Changes',129len(resources))130for file in resources:131job_set.started_job(file.path)132if file == self.resource:133changes.add_change(self._defining_file_changes(134changes, remove=remove, only_current=only_current))135else:136aim = None137if only_current and self.original == file:138aim = self.offset139handle = _InlineFunctionCallsForModuleHandle(140self.pycore, file, self.others_generator, aim)141result = move.ModuleSkipRenamer(142self.occurrence_finder, file, handle).get_changed_module()143if result is not None:144result = _add_imports(self.pycore, result,145file, self.imports)146if remove:147result = _remove_from(self.pycore, self.pyname,148result, file)149changes.add_change(ChangeContents(file, result))150job_set.finished_job()151return changes152153def _get_removed_range(self):154scope = self.pyfunction.get_scope()155lines = self.pymodule.lines156logical = self.pymodule.logical_lines157start_line = scope.get_start()158start, end = self._get_scope_range()159end_line = scope.get_end()160for i in range(end_line + 1, lines.length()):161if lines.get_line(i).strip() == '':162end_line = i163else:164break165end = min(lines.get_line_end(end_line) + 1,166len(self.pymodule.source_code))167return (start, end)168169def _defining_file_changes(self, changes, remove, only_current):170start_offset, end_offset = self._get_removed_range()171aim = None172if only_current:173if self.resource == self.original:174aim = self.offset175else:176# we don't want to change any of them177aim = len(self.resource.read()) + 100178handle = _InlineFunctionCallsForModuleHandle(179self.pycore, self.resource,180self.normal_generator, aim_offset=aim)181replacement = None182if remove:183replacement = self._get_method_replacement()184result = move.ModuleSkipRenamer(185self.occurrence_finder, self.resource, handle, start_offset,186end_offset, replacement).get_changed_module()187return ChangeContents(self.resource, result)188189def _get_method_replacement(self):190if self._is_the_last_method_of_a_class():191indents = sourceutils.get_indents(192self.pymodule.lines, self.pyfunction.get_scope().get_start())193return ' ' * indents + 'pass\n'194return ''195196def _is_the_last_method_of_a_class(self):197pyclass = self.pyfunction.parent198if not isinstance(pyclass, pyobjects.PyClass):199return False200class_start, class_end = sourceutils.get_body_region(pyclass)201source = self.pymodule.source_code202lines = self.pymodule.lines203func_start, func_end = self._get_scope_range()204if source[class_start:func_start].strip() == '' and \205source[func_end:class_end].strip() == '':206return True207return False208209def get_kind(self):210return 'method'211212213class InlineVariable(_Inliner):214215def __init__(self, *args, **kwds):216super(InlineVariable, self).__init__(*args, **kwds)217self.pymodule = self.pyname.get_definition_location()[0]218self.resource = self.pymodule.get_resource()219self._check_exceptional_conditions()220self._init_imports()221222def _check_exceptional_conditions(self):223if len(self.pyname.assignments) != 1:224raise rope.base.exceptions.RefactoringError(225'Local variable should be assigned once for inlining.')226227def get_changes(self, remove=True, only_current=False, resources=None,228task_handle=taskhandle.NullTaskHandle()):229if resources is None:230if rename._is_local(self.pyname):231resources = [self.resource]232else:233resources = self.pycore.get_python_files()234if only_current:235resources = [self.original]236if remove and self.original != self.resource:237resources.append(self.resource)238changes = ChangeSet('Inline variable <%s>' % self.name)239jobset = task_handle.create_jobset('Calculating changes',240len(resources))241242for resource in resources:243jobset.started_job(resource.path)244if resource == self.resource:245source = self._change_main_module(remove, only_current)246changes.add_change(ChangeContents(self.resource, source))247else:248result = self._change_module(resource, remove, only_current)249if result is not None:250result = _add_imports(self.pycore, result,251resource, self.imports)252changes.add_change(ChangeContents(resource, result))253jobset.finished_job()254return changes255256def _change_main_module(self, remove, only_current):257region = None258if only_current and self.original == self.resource:259region = self.region260return _inline_variable(self.pycore, self.pymodule, self.pyname,261self.name, remove=remove, region=region)262263def _init_imports(self):264vardef = _getvardef(self.pymodule, self.pyname)265self.imported, self.imports = move.moving_code_with_imports(266self.pycore, self.resource, vardef)267268def _change_module(self, resource, remove, only_current):269filters = [occurrences.NoImportsFilter(),270occurrences.PyNameFilter(self.pyname)]271if only_current and resource == self.original:272def check_aim(occurrence):273start, end = occurrence.get_primary_range()274if self.offset < start or end < self.offset:275return False276filters.insert(0, check_aim)277finder = occurrences.Finder(self.pycore, self.name, filters=filters)278changed = rename.rename_in_module(279finder, self.imported, resource=resource, replace_primary=True)280if changed and remove:281changed = _remove_from(self.pycore, self.pyname, changed, resource)282return changed283284def get_kind(self):285return 'variable'286287288class InlineParameter(_Inliner):289290def __init__(self, *args, **kwds):291super(InlineParameter, self).__init__(*args, **kwds)292resource, offset = self._function_location()293index = self.pyname.index294self.changers = [change_signature.ArgumentDefaultInliner(index)]295self.signature = change_signature.ChangeSignature(self.project,296resource, offset)297298def _function_location(self):299pymodule, lineno = self.pyname.get_definition_location()300resource = pymodule.get_resource()301start = pymodule.lines.get_line_start(lineno)302word_finder = worder.Worder(pymodule.source_code)303offset = word_finder.find_function_offset(start)304return resource, offset305306def get_changes(self, **kwds):307"""Get the changes needed by this refactoring308309See `rope.refactor.change_signature.ChangeSignature.get_changes()`310for arguments.311"""312return self.signature.get_changes(self.changers, **kwds)313314def get_kind(self):315return 'parameter'316317318def _join_lines(lines):319definition_lines = []320for unchanged_line in lines:321line = unchanged_line.strip()322if line.endswith('\\'):323line = line[:-1].strip()324definition_lines.append(line)325joined = ' '.join(definition_lines)326return joined327328329class _DefinitionGenerator(object):330unique_prefix = unique_prefix()331def __init__(self, project, pyfunction, body=None):332self.pycore = project.pycore333self.pyfunction = pyfunction334self.pymodule = pyfunction.get_module()335self.resource = self.pymodule.get_resource()336self.definition_info = self._get_definition_info()337self.definition_params = self._get_definition_params()338self._calculated_definitions = {}339if body is not None:340self.body = body341else:342self.body = sourceutils.get_body(self.pyfunction)343344def _get_definition_info(self):345return rope.refactor.functionutils.DefinitionInfo.read(self.pyfunction)346347def _get_definition_params(self):348definition_info = self.definition_info349paramdict = dict([pair for pair in definition_info.args_with_defaults])350if definition_info.args_arg is not None or \351definition_info.keywords_arg is not None:352raise rope.base.exceptions.RefactoringError(353'Cannot inline functions with list and keyword arguements.')354if self.pyfunction.get_kind() == 'classmethod':355paramdict[definition_info.args_with_defaults[0][0]] = \356self.pyfunction.parent.get_name()357return paramdict358359def get_function_name(self):360return self.pyfunction.get_name()361362def get_definition(self, primary, pyname, call, host_vars=[],returns=False):363# caching already calculated definitions364return self._calculate_definition(primary, pyname, call,365host_vars, returns)366367def _calculate_header(self, primary, pyname, call):368# A header is created which initializes parameters369# to the values passed to the function.370call_info = rope.refactor.functionutils.CallInfo.read(371primary, pyname, self.definition_info, call)372paramdict = self.definition_params373mapping = rope.refactor.functionutils.ArgumentMapping(374self.definition_info, call_info)375for param_name, value in mapping.param_dict.items():376paramdict[param_name] = value377header = ''378to_be_inlined = []379mod = self.pycore.get_string_module(self.body)380all_names = mod.get_scope().get_names()381assigned_names = [name for name in all_names if382isinstance(all_names[name], rope.base.pynamesdef.AssignedName)]383for name, value in paramdict.items():384if name != value and value is not None:385header += name + ' = ' + value.replace('\n', ' ') + '\n'386to_be_inlined.append(name)387return header, to_be_inlined388389def _calculate_definition(self, primary, pyname, call, host_vars, returns):390391header, to_be_inlined = self._calculate_header(primary, pyname, call)392393source = header + self.body394mod = self.pycore.get_string_module(source)395name_dict = mod.get_scope().get_names()396all_names = [x for x in name_dict if397not isinstance(name_dict[x], rope.base.builtins.BuiltinName)]398399# If there is a name conflict, all variable names400# inside the inlined function are renamed401if len(set(all_names).intersection(set(host_vars))) > 0:402403prefix = _DefinitionGenerator.unique_prefix.next()404guest = self.pycore.get_string_module(source, self.resource)405406to_be_inlined = [prefix+item for item in to_be_inlined]407for item in all_names:408pyname = guest[item]409occurrence_finder = occurrences.create_finder(410self.pycore, item, pyname)411source = rename.rename_in_module(occurrence_finder,412prefix+item, pymodule=guest)413guest = self.pycore.get_string_module(source, self.resource)414415#parameters not reassigned inside the functions are now inlined.416for name in to_be_inlined:417pymodule = self.pycore.get_string_module(source, self.resource)418pyname = pymodule[name]419source = _inline_variable(self.pycore, pymodule, pyname, name)420421return self._replace_returns_with(source, returns)422423def _replace_returns_with(self, source, returns):424result = []425returned = None426last_changed = 0427for match in _DefinitionGenerator._get_return_pattern().finditer(source):428for key, value in match.groupdict().items():429if value and key == 'return':430result.append(source[last_changed:match.start('return')])431if returns:432self._check_nothing_after_return(source,433match.end('return'))434returned = _join_lines(435source[match.end('return'): len(source)].splitlines())436last_changed = len(source)437else:438current = match.end('return')439while current < len(source) and source[current] in ' \t':440current += 1441last_changed = current442if current == len(source) or source[current] == '\n':443result.append('pass')444result.append(source[last_changed:])445return ''.join(result), returned446447def _check_nothing_after_return(self, source, offset):448lines = codeanalyze.SourceLinesAdapter(source)449lineno = lines.get_line_number(offset)450logical_lines = codeanalyze.LogicalLineFinder(lines)451lineno = logical_lines.logical_line_in(lineno)[1]452if source[lines.get_line_end(lineno):len(source)].strip() != '':453raise rope.base.exceptions.RefactoringError(454'Cannot inline functions with statements after return statement.')455456@classmethod457def _get_return_pattern(cls):458if not hasattr(cls, '_return_pattern'):459def named_pattern(name, list_):460return "(?P<%s>" % name + "|".join(list_) + ")"461comment_pattern = named_pattern('comment', [r'#[^\n]*'])462string_pattern = named_pattern('string',463[codeanalyze.get_string_pattern()])464return_pattern = r'\b(?P<return>return)\b'465cls._return_pattern = re.compile(comment_pattern + "|" +466string_pattern + "|" +467return_pattern)468return cls._return_pattern469470471class _InlineFunctionCallsForModuleHandle(object):472473def __init__(self, pycore, resource,474definition_generator, aim_offset=None):475"""Inlines occurrences476477If `aim` is not `None` only the occurrences that intersect478`aim` offset will be inlined.479480"""481self.pycore = pycore482self.generator = definition_generator483self.resource = resource484self.aim = aim_offset485486def occurred_inside_skip(self, change_collector, occurrence):487if not occurrence.is_defined():488raise rope.base.exceptions.RefactoringError(489'Cannot inline functions that reference themselves')490491def occurred_outside_skip(self, change_collector, occurrence):492start, end = occurrence.get_primary_range()493# we remove out of date imports later494if occurrence.is_in_import_statement():495return496# the function is referenced outside an import statement497if not occurrence.is_called():498raise rope.base.exceptions.RefactoringError(499'Reference to inlining function other than function call'500' in <file: %s, offset: %d>' % (self.resource.path, start))501if self.aim is not None and (self.aim < start or self.aim > end):502return503end_parens = self._find_end_parens(self.source, end - 1)504lineno = self.lines.get_line_number(start)505start_line, end_line = self.pymodule.logical_lines.\506logical_line_in(lineno)507line_start = self.lines.get_line_start(start_line)508line_end = self.lines.get_line_end(end_line)509510511returns = self.source[line_start:start].strip() != '' or \512self.source[end_parens:line_end].strip() != ''513indents = sourceutils.get_indents(self.lines, start_line)514primary, pyname = occurrence.get_primary_and_pyname()515516host = self.pycore.resource_to_pyobject(self.resource)517scope = host.scope.get_inner_scope_for_line(lineno)518definition, returned = self.generator.get_definition(519primary, pyname, self.source[start:end_parens], scope.get_names(), returns=returns)520521end = min(line_end + 1, len(self.source))522change_collector.add_change(line_start, end,523sourceutils.fix_indentation(definition, indents))524if returns:525name = returned526if name is None:527name = 'None'528change_collector.add_change(529line_end, end, self.source[line_start:start] + name +530self.source[end_parens:end])531532def _find_end_parens(self, source, offset):533finder = worder.Worder(source)534return finder.get_word_parens_range(offset)[1]535536@property537@utils.saveit538def pymodule(self):539return self.pycore.resource_to_pyobject(self.resource)540541@property542@utils.saveit543def source(self):544if self.resource is not None:545return self.resource.read()546else:547return self.pymodule.source_code548549@property550@utils.saveit551def lines(self):552return self.pymodule.lines553554555def _inline_variable(pycore, pymodule, pyname, name,556remove=True, region=None):557definition = _getvardef(pymodule, pyname)558start, end = _assigned_lineno(pymodule, pyname)559560occurrence_finder = occurrences.create_finder(pycore, name, pyname)561changed_source = rename.rename_in_module(562occurrence_finder, definition, pymodule=pymodule,563replace_primary=True, writes=False, region=region)564if changed_source is None:565changed_source = pymodule.source_code566if remove:567lines = codeanalyze.SourceLinesAdapter(changed_source)568source = changed_source[:lines.get_line_start(start)] + \569changed_source[lines.get_line_end(end) + 1:]570else:571source = changed_source572return source573574def _getvardef(pymodule, pyname):575assignment = pyname.assignments[0]576lines = pymodule.lines577start, end = _assigned_lineno(pymodule, pyname)578definition_with_assignment = _join_lines(579[lines.get_line(n) for n in range(start, end + 1)])580if assignment.levels:581raise rope.base.exceptions.RefactoringError(582'Cannot inline tuple assignments.')583definition = definition_with_assignment[definition_with_assignment.\584index('=') + 1:].strip()585return definition586587def _assigned_lineno(pymodule, pyname):588definition_line = pyname.assignments[0].ast_node.lineno589return pymodule.logical_lines.logical_line_in(definition_line)590591def _add_imports(pycore, source, resource, imports):592if not imports:593return source594pymodule = pycore.get_string_module(source, resource)595module_import = importutils.get_module_imports(pycore, pymodule)596for import_info in imports:597module_import.add_import(import_info)598source = module_import.get_changed_source()599pymodule = pycore.get_string_module(source, resource)600import_tools = importutils.ImportTools(pycore)601return import_tools.organize_imports(pymodule, unused=False, sort=False)602603def _get_pyname(pycore, resource, offset):604pymodule = pycore.resource_to_pyobject(resource)605pyname = evaluate.eval_location(pymodule, offset)606if isinstance(pyname, pynames.ImportedName):607pyname = pyname._get_imported_pyname()608return pyname609610def _remove_from(pycore, pyname, source, resource):611pymodule = pycore.get_string_module(source, resource)612module_import = importutils.get_module_imports(pycore, pymodule)613module_import.remove_pyname(pyname)614return module_import.get_changed_source()615616617