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() |