| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- # Known Bugs when inlining a function/method
- # The values passed to function are inlined using _inlined_variable.
- # This may cause two problems, illustrated in the examples below
- #
- # def foo(var1):
- # var1 = var1*10
- # return var1
- #
- # If a call to foo(20) is inlined, the result of inlined function is 20,
- # but it should be 200.
- #
- # def foo(var1):
- # var2 = var1*10
- # return var2
- #
- # 2- If a call to foo(10+10) is inlined the result of inlined function is 110
- # but it should be 200.
- import re
- import rope.base.exceptions
- import rope.refactor.functionutils
- from rope.base import (pynames, pyobjects, codeanalyze,
- taskhandle, evaluate, worder, utils)
- from rope.base.change import ChangeSet, ChangeContents
- from rope.refactor import (occurrences, rename, sourceutils,
- importutils, move, change_signature)
- def unique_prefix():
- n = 0
- while True:
- yield "__" + str(n) + "__"
- n += 1
- def create_inline(project, resource, offset):
- """Create a refactoring object for inlining
- Based on `resource` and `offset` it returns an instance of
- `InlineMethod`, `InlineVariable` or `InlineParameter`.
- """
- pycore = project.pycore
- pyname = _get_pyname(pycore, resource, offset)
- message = 'Inline refactoring should be performed on ' \
- 'a method, local variable or parameter.'
- if pyname is None:
- raise rope.base.exceptions.RefactoringError(message)
- if isinstance(pyname, pynames.ImportedName):
- pyname = pyname._get_imported_pyname()
- if isinstance(pyname, pynames.AssignedName):
- return InlineVariable(project, resource, offset)
- if isinstance(pyname, pynames.ParameterName):
- return InlineParameter(project, resource, offset)
- if isinstance(pyname.get_object(), pyobjects.PyFunction):
- return InlineMethod(project, resource, offset)
- else:
- raise rope.base.exceptions.RefactoringError(message)
- class _Inliner(object):
- def __init__(self, project, resource, offset):
- self.project = project
- self.pycore = project.pycore
- self.pyname = _get_pyname(self.pycore, resource, offset)
- range_finder = worder.Worder(resource.read())
- self.region = range_finder.get_primary_range(offset)
- self.name = range_finder.get_word_at(offset)
- self.offset = offset
- self.original = resource
- def get_changes(self, *args, **kwds):
- pass
- def get_kind(self):
- """Return either 'variable', 'method' or 'parameter'"""
- class InlineMethod(_Inliner):
- def __init__(self, *args, **kwds):
- super(InlineMethod, self).__init__(*args, **kwds)
- self.pyfunction = self.pyname.get_object()
- self.pymodule = self.pyfunction.get_module()
- self.resource = self.pyfunction.get_module().get_resource()
- self.occurrence_finder = occurrences.create_finder(
- self.pycore, self.name, self.pyname)
- self.normal_generator = _DefinitionGenerator(self.project,
- self.pyfunction)
- self._init_imports()
- def _init_imports(self):
- body = sourceutils.get_body(self.pyfunction)
- body, imports = move.moving_code_with_imports(
- self.pycore, self.resource, body)
- self.imports = imports
- self.others_generator = _DefinitionGenerator(
- self.project, self.pyfunction, body=body)
- def _get_scope_range(self):
- scope = self.pyfunction.get_scope()
- lines = self.pymodule.lines
- logicals = self.pymodule.logical_lines
- start_line = scope.get_start()
- if self.pyfunction.decorators:
- decorators = self.pyfunction.decorators
- if hasattr(decorators[0], 'lineno'):
- start_line = decorators[0].lineno
- start_offset = lines.get_line_start(start_line)
- end_offset = min(lines.get_line_end(scope.end) + 1,
- len(self.pymodule.source_code))
- return (start_offset, end_offset)
- def get_changes(self, remove=True, only_current=False, resources=None,
- task_handle=taskhandle.NullTaskHandle()):
- """Get the changes this refactoring makes
- If `remove` is `False` the definition will not be removed. If
- `only_current` is `True`, the the current occurrence will be
- inlined, only.
- """
- changes = ChangeSet('Inline method <%s>' % self.name)
- if resources is None:
- resources = self.pycore.get_python_files()
- if only_current:
- resources = [self.original]
- if remove:
- resources.append(self.resource)
- job_set = task_handle.create_jobset('Collecting Changes',
- len(resources))
- for file in resources:
- job_set.started_job(file.path)
- if file == self.resource:
- changes.add_change(self._defining_file_changes(
- changes, remove=remove, only_current=only_current))
- else:
- aim = None
- if only_current and self.original == file:
- aim = self.offset
- handle = _InlineFunctionCallsForModuleHandle(
- self.pycore, file, self.others_generator, aim)
- result = move.ModuleSkipRenamer(
- self.occurrence_finder, file, handle).get_changed_module()
- if result is not None:
- result = _add_imports(self.pycore, result,
- file, self.imports)
- if remove:
- result = _remove_from(self.pycore, self.pyname,
- result, file)
- changes.add_change(ChangeContents(file, result))
- job_set.finished_job()
- return changes
- def _get_removed_range(self):
- scope = self.pyfunction.get_scope()
- lines = self.pymodule.lines
- logical = self.pymodule.logical_lines
- start_line = scope.get_start()
- start, end = self._get_scope_range()
- end_line = scope.get_end()
- for i in range(end_line + 1, lines.length()):
- if lines.get_line(i).strip() == '':
- end_line = i
- else:
- break
- end = min(lines.get_line_end(end_line) + 1,
- len(self.pymodule.source_code))
- return (start, end)
- def _defining_file_changes(self, changes, remove, only_current):
- start_offset, end_offset = self._get_removed_range()
- aim = None
- if only_current:
- if self.resource == self.original:
- aim = self.offset
- else:
- # we don't want to change any of them
- aim = len(self.resource.read()) + 100
- handle = _InlineFunctionCallsForModuleHandle(
- self.pycore, self.resource,
- self.normal_generator, aim_offset=aim)
- replacement = None
- if remove:
- replacement = self._get_method_replacement()
- result = move.ModuleSkipRenamer(
- self.occurrence_finder, self.resource, handle, start_offset,
- end_offset, replacement).get_changed_module()
- return ChangeContents(self.resource, result)
- def _get_method_replacement(self):
- if self._is_the_last_method_of_a_class():
- indents = sourceutils.get_indents(
- self.pymodule.lines, self.pyfunction.get_scope().get_start())
- return ' ' * indents + 'pass\n'
- return ''
- def _is_the_last_method_of_a_class(self):
- pyclass = self.pyfunction.parent
- if not isinstance(pyclass, pyobjects.PyClass):
- return False
- class_start, class_end = sourceutils.get_body_region(pyclass)
- source = self.pymodule.source_code
- lines = self.pymodule.lines
- func_start, func_end = self._get_scope_range()
- if source[class_start:func_start].strip() == '' and \
- source[func_end:class_end].strip() == '':
- return True
- return False
- def get_kind(self):
- return 'method'
- class InlineVariable(_Inliner):
- def __init__(self, *args, **kwds):
- super(InlineVariable, self).__init__(*args, **kwds)
- self.pymodule = self.pyname.get_definition_location()[0]
- self.resource = self.pymodule.get_resource()
- self._check_exceptional_conditions()
- self._init_imports()
- def _check_exceptional_conditions(self):
- if len(self.pyname.assignments) != 1:
- raise rope.base.exceptions.RefactoringError(
- 'Local variable should be assigned once for inlining.')
- def get_changes(self, remove=True, only_current=False, resources=None,
- task_handle=taskhandle.NullTaskHandle()):
- if resources is None:
- if rename._is_local(self.pyname):
- resources = [self.resource]
- else:
- resources = self.pycore.get_python_files()
- if only_current:
- resources = [self.original]
- if remove and self.original != self.resource:
- resources.append(self.resource)
- changes = ChangeSet('Inline variable <%s>' % self.name)
- jobset = task_handle.create_jobset('Calculating changes',
- len(resources))
- for resource in resources:
- jobset.started_job(resource.path)
- if resource == self.resource:
- source = self._change_main_module(remove, only_current)
- changes.add_change(ChangeContents(self.resource, source))
- else:
- result = self._change_module(resource, remove, only_current)
- if result is not None:
- result = _add_imports(self.pycore, result,
- resource, self.imports)
- changes.add_change(ChangeContents(resource, result))
- jobset.finished_job()
- return changes
- def _change_main_module(self, remove, only_current):
- region = None
- if only_current and self.original == self.resource:
- region = self.region
- return _inline_variable(self.pycore, self.pymodule, self.pyname,
- self.name, remove=remove, region=region)
- def _init_imports(self):
- vardef = _getvardef(self.pymodule, self.pyname)
- self.imported, self.imports = move.moving_code_with_imports(
- self.pycore, self.resource, vardef)
- def _change_module(self, resource, remove, only_current):
- filters = [occurrences.NoImportsFilter(),
- occurrences.PyNameFilter(self.pyname)]
- if only_current and resource == self.original:
- def check_aim(occurrence):
- start, end = occurrence.get_primary_range()
- if self.offset < start or end < self.offset:
- return False
- filters.insert(0, check_aim)
- finder = occurrences.Finder(self.pycore, self.name, filters=filters)
- changed = rename.rename_in_module(
- finder, self.imported, resource=resource, replace_primary=True)
- if changed and remove:
- changed = _remove_from(self.pycore, self.pyname, changed, resource)
- return changed
- def get_kind(self):
- return 'variable'
- class InlineParameter(_Inliner):
- def __init__(self, *args, **kwds):
- super(InlineParameter, self).__init__(*args, **kwds)
- resource, offset = self._function_location()
- index = self.pyname.index
- self.changers = [change_signature.ArgumentDefaultInliner(index)]
- self.signature = change_signature.ChangeSignature(self.project,
- resource, offset)
- def _function_location(self):
- pymodule, lineno = self.pyname.get_definition_location()
- resource = pymodule.get_resource()
- start = pymodule.lines.get_line_start(lineno)
- word_finder = worder.Worder(pymodule.source_code)
- offset = word_finder.find_function_offset(start)
- return resource, offset
- def get_changes(self, **kwds):
- """Get the changes needed by this refactoring
- See `rope.refactor.change_signature.ChangeSignature.get_changes()`
- for arguments.
- """
- return self.signature.get_changes(self.changers, **kwds)
- def get_kind(self):
- return 'parameter'
- def _join_lines(lines):
- definition_lines = []
- for unchanged_line in lines:
- line = unchanged_line.strip()
- if line.endswith('\\'):
- line = line[:-1].strip()
- definition_lines.append(line)
- joined = ' '.join(definition_lines)
- return joined
- class _DefinitionGenerator(object):
- unique_prefix = unique_prefix()
- def __init__(self, project, pyfunction, body=None):
- self.pycore = project.pycore
- self.pyfunction = pyfunction
- self.pymodule = pyfunction.get_module()
- self.resource = self.pymodule.get_resource()
- self.definition_info = self._get_definition_info()
- self.definition_params = self._get_definition_params()
- self._calculated_definitions = {}
- if body is not None:
- self.body = body
- else:
- self.body = sourceutils.get_body(self.pyfunction)
- def _get_definition_info(self):
- return rope.refactor.functionutils.DefinitionInfo.read(self.pyfunction)
- def _get_definition_params(self):
- definition_info = self.definition_info
- paramdict = dict([pair for pair in definition_info.args_with_defaults])
- if definition_info.args_arg is not None or \
- definition_info.keywords_arg is not None:
- raise rope.base.exceptions.RefactoringError(
- 'Cannot inline functions with list and keyword arguements.')
- if self.pyfunction.get_kind() == 'classmethod':
- paramdict[definition_info.args_with_defaults[0][0]] = \
- self.pyfunction.parent.get_name()
- return paramdict
- def get_function_name(self):
- return self.pyfunction.get_name()
- def get_definition(self, primary, pyname, call, host_vars=[],returns=False):
- # caching already calculated definitions
- return self._calculate_definition(primary, pyname, call,
- host_vars, returns)
- def _calculate_header(self, primary, pyname, call):
- # A header is created which initializes parameters
- # to the values passed to the function.
- call_info = rope.refactor.functionutils.CallInfo.read(
- primary, pyname, self.definition_info, call)
- paramdict = self.definition_params
- mapping = rope.refactor.functionutils.ArgumentMapping(
- self.definition_info, call_info)
- for param_name, value in mapping.param_dict.items():
- paramdict[param_name] = value
- header = ''
- to_be_inlined = []
- mod = self.pycore.get_string_module(self.body)
- all_names = mod.get_scope().get_names()
- assigned_names = [name for name in all_names if
- isinstance(all_names[name], rope.base.pynamesdef.AssignedName)]
- for name, value in paramdict.items():
- if name != value and value is not None:
- header += name + ' = ' + value.replace('\n', ' ') + '\n'
- to_be_inlined.append(name)
- return header, to_be_inlined
- def _calculate_definition(self, primary, pyname, call, host_vars, returns):
- header, to_be_inlined = self._calculate_header(primary, pyname, call)
- source = header + self.body
- mod = self.pycore.get_string_module(source)
- name_dict = mod.get_scope().get_names()
- all_names = [x for x in name_dict if
- not isinstance(name_dict[x], rope.base.builtins.BuiltinName)]
- # If there is a name conflict, all variable names
- # inside the inlined function are renamed
- if len(set(all_names).intersection(set(host_vars))) > 0:
- prefix = _DefinitionGenerator.unique_prefix.next()
- guest = self.pycore.get_string_module(source, self.resource)
- to_be_inlined = [prefix+item for item in to_be_inlined]
- for item in all_names:
- pyname = guest[item]
- occurrence_finder = occurrences.create_finder(
- self.pycore, item, pyname)
- source = rename.rename_in_module(occurrence_finder,
- prefix+item, pymodule=guest)
- guest = self.pycore.get_string_module(source, self.resource)
- #parameters not reassigned inside the functions are now inlined.
- for name in to_be_inlined:
- pymodule = self.pycore.get_string_module(source, self.resource)
- pyname = pymodule[name]
- source = _inline_variable(self.pycore, pymodule, pyname, name)
- return self._replace_returns_with(source, returns)
- def _replace_returns_with(self, source, returns):
- result = []
- returned = None
- last_changed = 0
- for match in _DefinitionGenerator._get_return_pattern().finditer(source):
- for key, value in match.groupdict().items():
- if value and key == 'return':
- result.append(source[last_changed:match.start('return')])
- if returns:
- self._check_nothing_after_return(source,
- match.end('return'))
- returned = _join_lines(
- source[match.end('return'): len(source)].splitlines())
- last_changed = len(source)
- else:
- current = match.end('return')
- while current < len(source) and source[current] in ' \t':
- current += 1
- last_changed = current
- if current == len(source) or source[current] == '\n':
- result.append('pass')
- result.append(source[last_changed:])
- return ''.join(result), returned
- def _check_nothing_after_return(self, source, offset):
- lines = codeanalyze.SourceLinesAdapter(source)
- lineno = lines.get_line_number(offset)
- logical_lines = codeanalyze.LogicalLineFinder(lines)
- lineno = logical_lines.logical_line_in(lineno)[1]
- if source[lines.get_line_end(lineno):len(source)].strip() != '':
- raise rope.base.exceptions.RefactoringError(
- 'Cannot inline functions with statements after return statement.')
- @classmethod
- def _get_return_pattern(cls):
- if not hasattr(cls, '_return_pattern'):
- def named_pattern(name, list_):
- return "(?P<%s>" % name + "|".join(list_) + ")"
- comment_pattern = named_pattern('comment', [r'#[^\n]*'])
- string_pattern = named_pattern('string',
- [codeanalyze.get_string_pattern()])
- return_pattern = r'\b(?P<return>return)\b'
- cls._return_pattern = re.compile(comment_pattern + "|" +
- string_pattern + "|" +
- return_pattern)
- return cls._return_pattern
- class _InlineFunctionCallsForModuleHandle(object):
- def __init__(self, pycore, resource,
- definition_generator, aim_offset=None):
- """Inlines occurrences
- If `aim` is not `None` only the occurrences that intersect
- `aim` offset will be inlined.
- """
- self.pycore = pycore
- self.generator = definition_generator
- self.resource = resource
- self.aim = aim_offset
- def occurred_inside_skip(self, change_collector, occurrence):
- if not occurrence.is_defined():
- raise rope.base.exceptions.RefactoringError(
- 'Cannot inline functions that reference themselves')
- def occurred_outside_skip(self, change_collector, occurrence):
- start, end = occurrence.get_primary_range()
- # we remove out of date imports later
- if occurrence.is_in_import_statement():
- return
- # the function is referenced outside an import statement
- if not occurrence.is_called():
- raise rope.base.exceptions.RefactoringError(
- 'Reference to inlining function other than function call'
- ' in <file: %s, offset: %d>' % (self.resource.path, start))
- if self.aim is not None and (self.aim < start or self.aim > end):
- return
- end_parens = self._find_end_parens(self.source, end - 1)
- lineno = self.lines.get_line_number(start)
- start_line, end_line = self.pymodule.logical_lines.\
- logical_line_in(lineno)
- line_start = self.lines.get_line_start(start_line)
- line_end = self.lines.get_line_end(end_line)
- returns = self.source[line_start:start].strip() != '' or \
- self.source[end_parens:line_end].strip() != ''
- indents = sourceutils.get_indents(self.lines, start_line)
- primary, pyname = occurrence.get_primary_and_pyname()
- host = self.pycore.resource_to_pyobject(self.resource)
- scope = host.scope.get_inner_scope_for_line(lineno)
- definition, returned = self.generator.get_definition(
- primary, pyname, self.source[start:end_parens], scope.get_names(), returns=returns)
- end = min(line_end + 1, len(self.source))
- change_collector.add_change(line_start, end,
- sourceutils.fix_indentation(definition, indents))
- if returns:
- name = returned
- if name is None:
- name = 'None'
- change_collector.add_change(
- line_end, end, self.source[line_start:start] + name +
- self.source[end_parens:end])
- def _find_end_parens(self, source, offset):
- finder = worder.Worder(source)
- return finder.get_word_parens_range(offset)[1]
- @property
- @utils.saveit
- def pymodule(self):
- return self.pycore.resource_to_pyobject(self.resource)
- @property
- @utils.saveit
- def source(self):
- if self.resource is not None:
- return self.resource.read()
- else:
- return self.pymodule.source_code
- @property
- @utils.saveit
- def lines(self):
- return self.pymodule.lines
- def _inline_variable(pycore, pymodule, pyname, name,
- remove=True, region=None):
- definition = _getvardef(pymodule, pyname)
- start, end = _assigned_lineno(pymodule, pyname)
- occurrence_finder = occurrences.create_finder(pycore, name, pyname)
- changed_source = rename.rename_in_module(
- occurrence_finder, definition, pymodule=pymodule,
- replace_primary=True, writes=False, region=region)
- if changed_source is None:
- changed_source = pymodule.source_code
- if remove:
- lines = codeanalyze.SourceLinesAdapter(changed_source)
- source = changed_source[:lines.get_line_start(start)] + \
- changed_source[lines.get_line_end(end) + 1:]
- else:
- source = changed_source
- return source
- def _getvardef(pymodule, pyname):
- assignment = pyname.assignments[0]
- lines = pymodule.lines
- start, end = _assigned_lineno(pymodule, pyname)
- definition_with_assignment = _join_lines(
- [lines.get_line(n) for n in range(start, end + 1)])
- if assignment.levels:
- raise rope.base.exceptions.RefactoringError(
- 'Cannot inline tuple assignments.')
- definition = definition_with_assignment[definition_with_assignment.\
- index('=') + 1:].strip()
- return definition
- def _assigned_lineno(pymodule, pyname):
- definition_line = pyname.assignments[0].ast_node.lineno
- return pymodule.logical_lines.logical_line_in(definition_line)
- def _add_imports(pycore, source, resource, imports):
- if not imports:
- return source
- pymodule = pycore.get_string_module(source, resource)
- module_import = importutils.get_module_imports(pycore, pymodule)
- for import_info in imports:
- module_import.add_import(import_info)
- source = module_import.get_changed_source()
- pymodule = pycore.get_string_module(source, resource)
- import_tools = importutils.ImportTools(pycore)
- return import_tools.organize_imports(pymodule, unused=False, sort=False)
- def _get_pyname(pycore, resource, offset):
- pymodule = pycore.resource_to_pyobject(resource)
- pyname = evaluate.eval_location(pymodule, offset)
- if isinstance(pyname, pynames.ImportedName):
- pyname = pyname._get_imported_pyname()
- return pyname
- def _remove_from(pycore, pyname, source, resource):
- pymodule = pycore.get_string_module(source, resource)
- module_import = importutils.get_module_imports(pycore, pymodule)
- module_import.remove_pyname(pyname)
- return module_import.get_changed_source()
|