Path: blob/master/elisp/emacs-for-python/rope-dist/rope/refactor/similarfinder.py
1494 views
"""This module can be used for finding similar code"""1import re23import rope.refactor.wildcards4from rope.base import codeanalyze, evaluate, exceptions, ast, builtins5from rope.refactor import (patchedast, sourceutils, occurrences,6wildcards, importutils)789class BadNameInCheckError(exceptions.RefactoringError):10pass111213class SimilarFinder(object):14"""`SimilarFinder` can be used to find similar pieces of code1516See the notes in the `rope.refactor.restructure` module for more17info.1819"""2021def __init__(self, pymodule, wildcards=None):22"""Construct a SimilarFinder"""23self.source = pymodule.source_code24self.raw_finder = RawSimilarFinder(25pymodule.source_code, pymodule.get_ast(), self._does_match)26self.pymodule = pymodule27if wildcards is None:28self.wildcards = {}29for wildcard in [rope.refactor.wildcards.30DefaultWildcard(pymodule.pycore.project)]:31self.wildcards[wildcard.get_name()] = wildcard32else:33self.wildcards = wildcards3435def get_matches(self, code, args={}, start=0, end=None):36self.args = args37if end is None:38end = len(self.source)39skip_region = None40if 'skip' in args.get('', {}):41resource, region = args['']['skip']42if resource == self.pymodule.get_resource():43skip_region = region44return self.raw_finder.get_matches(code, start=start, end=end,45skip=skip_region)4647def get_match_regions(self, *args, **kwds):48for match in self.get_matches(*args, **kwds):49yield match.get_region()5051def _does_match(self, node, name):52arg = self.args.get(name, '')53kind = 'default'54if isinstance(arg, (tuple, list)):55kind = arg[0]56arg = arg[1]57suspect = wildcards.Suspect(self.pymodule, node, name)58return self.wildcards[kind].matches(suspect, arg)596061class RawSimilarFinder(object):62"""A class for finding similar expressions and statements"""6364def __init__(self, source, node=None, does_match=None):65if node is None:66node = ast.parse(source)67if does_match is None:68self.does_match = self._simple_does_match69else:70self.does_match = does_match71self._init_using_ast(node, source)7273def _simple_does_match(self, node, name):74return isinstance(node, (ast.expr, ast.Name))7576def _init_using_ast(self, node, source):77self.source = source78self._matched_asts = {}79if not hasattr(node, 'region'):80patchedast.patch_ast(node, source)81self.ast = node8283def get_matches(self, code, start=0, end=None, skip=None):84"""Search for `code` in source and return a list of `Match`\es8586`code` can contain wildcards. ``${name}`` matches normal87names and ``${?name} can match any expression. You can use88`Match.get_ast()` for getting the node that has matched a89given pattern.9091"""92if end is None:93end = len(self.source)94for match in self._get_matched_asts(code):95match_start, match_end = match.get_region()96if start <= match_start and match_end <= end:97if skip is not None and (skip[0] < match_end and98skip[1] > match_start):99continue100yield match101102def _get_matched_asts(self, code):103if code not in self._matched_asts:104wanted = self._create_pattern(code)105matches = _ASTMatcher(self.ast, wanted,106self.does_match).find_matches()107self._matched_asts[code] = matches108return self._matched_asts[code]109110def _create_pattern(self, expression):111expression = self._replace_wildcards(expression)112node = ast.parse(expression)113# Getting Module.Stmt.nodes114nodes = node.body115if len(nodes) == 1 and isinstance(nodes[0], ast.Expr):116# Getting Discard.expr117wanted = nodes[0].value118else:119wanted = nodes120return wanted121122def _replace_wildcards(self, expression):123ropevar = _RopeVariable()124template = CodeTemplate(expression)125mapping = {}126for name in template.get_names():127mapping[name] = ropevar.get_var(name)128return template.substitute(mapping)129130131class _ASTMatcher(object):132133def __init__(self, body, pattern, does_match):134"""Searches the given pattern in the body AST.135136body is an AST node and pattern can be either an AST node or137a list of ASTs nodes138"""139self.body = body140self.pattern = pattern141self.matches = None142self.ropevar = _RopeVariable()143self.matches_callback = does_match144145def find_matches(self):146if self.matches is None:147self.matches = []148ast.call_for_nodes(self.body, self._check_node, recursive=True)149return self.matches150151def _check_node(self, node):152if isinstance(self.pattern, list):153self._check_statements(node)154else:155self._check_expression(node)156157def _check_expression(self, node):158mapping = {}159if self._match_nodes(self.pattern, node, mapping):160self.matches.append(ExpressionMatch(node, mapping))161162def _check_statements(self, node):163for child in ast.get_children(node):164if isinstance(child, (list, tuple)):165self.__check_stmt_list(child)166167def __check_stmt_list(self, nodes):168for index in range(len(nodes)):169if len(nodes) - index >= len(self.pattern):170current_stmts = nodes[index:index + len(self.pattern)]171mapping = {}172if self._match_stmts(current_stmts, mapping):173self.matches.append(StatementMatch(current_stmts, mapping))174175def _match_nodes(self, expected, node, mapping):176if isinstance(expected, ast.Name):177if self.ropevar.is_var(expected.id):178return self._match_wildcard(expected, node, mapping)179if not isinstance(expected, ast.AST):180return expected == node181if expected.__class__ != node.__class__:182return False183184children1 = self._get_children(expected)185children2 = self._get_children(node)186if len(children1) != len(children2):187return False188for child1, child2 in zip(children1, children2):189if isinstance(child1, ast.AST):190if not self._match_nodes(child1, child2, mapping):191return False192elif isinstance(child1, (list, tuple)):193if not isinstance(child2, (list, tuple)) or \194len(child1) != len(child2):195return False196for c1, c2 in zip(child1, child2):197if not self._match_nodes(c1, c2, mapping):198return False199else:200if child1 != child2:201return False202return True203204def _get_children(self, node):205"""Return not `ast.expr_context` children of `node`"""206children = ast.get_children(node)207return [child for child in children208if not isinstance(child, ast.expr_context)]209210def _match_stmts(self, current_stmts, mapping):211if len(current_stmts) != len(self.pattern):212return False213for stmt, expected in zip(current_stmts, self.pattern):214if not self._match_nodes(expected, stmt, mapping):215return False216return True217218def _match_wildcard(self, node1, node2, mapping):219name = self.ropevar.get_base(node1.id)220if name not in mapping:221if self.matches_callback(node2, name):222mapping[name] = node2223return True224return False225else:226return self._match_nodes(mapping[name], node2, {})227228229class Match(object):230231def __init__(self, mapping):232self.mapping = mapping233234def get_region(self):235"""Returns match region"""236237def get_ast(self, name):238"""Return the ast node that has matched rope variables"""239return self.mapping.get(name, None)240241242class ExpressionMatch(Match):243244def __init__(self, ast, mapping):245super(ExpressionMatch, self).__init__(mapping)246self.ast = ast247248def get_region(self):249return self.ast.region250251252class StatementMatch(Match):253254def __init__(self, ast_list, mapping):255super(StatementMatch, self).__init__(mapping)256self.ast_list = ast_list257258def get_region(self):259return self.ast_list[0].region[0], self.ast_list[-1].region[1]260261262class CodeTemplate(object):263264def __init__(self, template):265self.template = template266self._find_names()267268def _find_names(self):269self.names = {}270for match in CodeTemplate._get_pattern().finditer(self.template):271if 'name' in match.groupdict() and \272match.group('name') is not None:273start, end = match.span('name')274name = self.template[start + 2:end - 1]275if name not in self.names:276self.names[name] = []277self.names[name].append((start, end))278279def get_names(self):280return self.names.keys()281282def substitute(self, mapping):283collector = codeanalyze.ChangeCollector(self.template)284for name, occurrences in self.names.items():285for region in occurrences:286collector.add_change(region[0], region[1], mapping[name])287result = collector.get_changed()288if result is None:289return self.template290return result291292_match_pattern = None293294@classmethod295def _get_pattern(cls):296if cls._match_pattern is None:297pattern = codeanalyze.get_comment_pattern() + '|' + \298codeanalyze.get_string_pattern() + '|' + \299r'(?P<name>\$\{[^\s\$\}]*\})'300cls._match_pattern = re.compile(pattern)301return cls._match_pattern302303304class _RopeVariable(object):305"""Transform and identify rope inserted wildcards"""306307_normal_prefix = '__rope__variable_normal_'308_any_prefix = '__rope__variable_any_'309310def get_var(self, name):311if name.startswith('?'):312return self._get_any(name)313else:314return self._get_normal(name)315316def is_var(self, name):317return self._is_normal(name) or self._is_var(name)318319def get_base(self, name):320if self._is_normal(name):321return name[len(self._normal_prefix):]322if self._is_var(name):323return '?' + name[len(self._any_prefix):]324325def _get_normal(self, name):326return self._normal_prefix + name327328def _get_any(self, name):329return self._any_prefix + name[1:]330331def _is_normal(self, name):332return name.startswith(self._normal_prefix)333334def _is_var(self, name):335return name.startswith(self._any_prefix)336337338def make_pattern(code, variables):339variables = set(variables)340collector = codeanalyze.ChangeCollector(code)341def does_match(node, name):342return isinstance(node, ast.Name) and node.id == name343finder = RawSimilarFinder(code, does_match=does_match)344for variable in variables:345for match in finder.get_matches('${%s}' % variable):346start, end = match.get_region()347collector.add_change(start, end, '${%s}' % variable)348result = collector.get_changed()349return result if result is not None else code350351352def _pydefined_to_str(pydefined):353address = []354if isinstance(pydefined, (builtins.BuiltinClass, builtins.BuiltinFunction)):355return '__builtins__.' + pydefined.get_name()356else:357while pydefined.parent is not None:358address.insert(0, pydefined.get_name())359pydefined = pydefined.parent360module_name = pydefined.pycore.modname(pydefined.resource)361return '.'.join(module_name.split('.') + address)362363364