Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(5)

Unified Diff: tools/telemetry/third_party/rope/rope/refactor/inline.py

Issue 1132103009: Example of refactoring using rope library. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tools/telemetry/third_party/rope/rope/refactor/inline.py
diff --git a/tools/telemetry/third_party/rope/rope/refactor/inline.py b/tools/telemetry/third_party/rope/rope/refactor/inline.py
new file mode 100644
index 0000000000000000000000000000000000000000..467edefaa16facf95ef77406a32ce54e95688fad
--- /dev/null
+++ b/tools/telemetry/third_party/rope/rope/refactor/inline.py
@@ -0,0 +1,625 @@
+# 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, libutils)
+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`.
+
+ """
+ pyname = _get_pyname(project, 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.pyname = _get_pyname(project, resource, offset)
+ range_finder = worder.Worder(resource.read(), True)
+ 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.project, 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.project, 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
+ 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.project.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.project, 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.project, result,
+ file, self.imports)
+ if remove:
+ result = _remove_from(self.project, 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
+ 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.project, 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
+ 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,
+ docs=False, task_handle=taskhandle.NullTaskHandle()):
+ if resources is None:
+ if rename._is_local(self.pyname):
+ resources = [self.resource]
+ else:
+ resources = self.project.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, docs)
+ 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.project, result,
+ resource, self.imports)
+ changes.add_change(ChangeContents(resource, result))
+ jobset.finished_job()
+ return changes
+
+ def _change_main_module(self, remove, only_current, docs):
+ region = None
+ if only_current and self.original == self.resource:
+ region = self.region
+ return _inline_variable(self.project, self.pymodule, self.pyname,
+ self.name, remove=remove, region=region,
+ docs=docs)
+
+ def _init_imports(self):
+ vardef = _getvardef(self.pymodule, self.pyname)
+ self.imported, self.imports = move.moving_code_with_imports(
+ self.project, 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.project, 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.project, 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.project = project
+ 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 = []
+ 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 = libutils.get_string_module(self.project, 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 = next(_DefinitionGenerator.unique_prefix)
+ guest = libutils.get_string_module(self.project, 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.project,
+ item, pyname)
+ source = rename.rename_in_module(occurrence_finder,
+ prefix + item, pymodule=guest)
+ guest = libutils.get_string_module(
+ self.project, source, self.resource)
+
+ #parameters not reassigned inside the functions are now inlined.
+ for name in to_be_inlined:
+ pymodule = libutils.get_string_module(
+ self.project, source, self.resource)
+ pyname = pymodule[name]
+ source = _inline_variable(self.project, 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'))
+ beg_idx = match.end('return')
+ returned = _join_lines(
+ source[beg_idx: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, project, resource,
+ definition_generator, aim_offset=None):
+ """Inlines occurrences
+
+ If `aim` is not `None` only the occurrences that intersect
+ `aim` offset will be inlined.
+
+ """
+ self.project = project
+ 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.pymodule
+ 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.project.get_pymodule(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(project, pymodule, pyname, name,
+ remove=True, region=None, docs=False):
+ definition = _getvardef(pymodule, pyname)
+ start, end = _assigned_lineno(pymodule, pyname)
+
+ occurrence_finder = occurrences.create_finder(project, name, pyname,
+ docs=docs)
+ 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(project, source, resource, imports):
+ if not imports:
+ return source
+ pymodule = libutils.get_string_module(project, source, resource)
+ module_import = importutils.get_module_imports(project, pymodule)
+ for import_info in imports:
+ module_import.add_import(import_info)
+ source = module_import.get_changed_source()
+ pymodule = libutils.get_string_module(project, source, resource)
+ import_tools = importutils.ImportTools(project)
+ return import_tools.organize_imports(pymodule, unused=False, sort=False)
+
+
+def _get_pyname(project, resource, offset):
+ pymodule = project.get_pymodule(resource)
+ pyname = evaluate.eval_location(pymodule, offset)
+ if isinstance(pyname, pynames.ImportedName):
+ pyname = pyname._get_imported_pyname()
+ return pyname
+
+
+def _remove_from(project, pyname, source, resource):
+ pymodule = libutils.get_string_module(project, source, resource)
+ module_import = importutils.get_module_imports(project, pymodule)
+ module_import.remove_pyname(pyname)
+ return module_import.get_changed_source()

Powered by Google App Engine
This is Rietveld 408576698