"""Find occurrences of a name in a project. This module consists of a `Finder` that finds all occurrences of a name in a project. The `Finder.find_occurrences()` method is a generator that yields `Occurrence` instances for each occurrence of the name. To create a `Finder` object, use the `create_finder()` function: finder = occurrences.create_finder(project, 'foo', pyname) for occurrence in finder.find_occurrences(): pass It's possible to filter the occurrences. They can be specified when calling the `create_finder()` function. * `only_calls`: If True, return only those instances where the name is a function that's being called. * `imports`: If False, don't return instances that are in import statements. * `unsure`: If a prediate function, return instances where we don't know what the name references. It also filters based on the predicate function. * `docs`: If True, it will search for occurrences in regions normally ignored. E.g., strings and comments. * `in_hierarchy`: If True, it will find occurrences if the name is in the class's hierarchy. * `instance`: Used only when you want implicit interfaces to be considered. * `keywords`: If False, don't return instances that are the names of keyword arguments """ import ast import re from rope.base import codeanalyze from rope.base import evaluate from rope.base import exceptions from rope.base import pynames from rope.base import pyobjects from rope.base import utils from rope.base import worder class Finder(object): """For finding occurrences of a name The constructor takes a `filters` argument. It should be a list of functions that take a single argument. For each possible occurrence, these functions are called in order with the an instance of `Occurrence`: * If it returns `None` other filters are tried. * If it returns `True`, the occurrence will be a match. * If it returns `False`, the occurrence will be skipped. * If all of the filters return `None`, it is skipped also. """ def __init__(self, project, name, filters=[lambda o: True], docs=False): self.project = project self.name = name self.docs = docs self.filters = filters self._textual_finder = _TextualFinder(name, docs=docs) def find_occurrences(self, resource=None, pymodule=None): """Generate `Occurrence` instances""" tools = _OccurrenceToolsCreator( self.project, resource=resource, pymodule=pymodule, docs=self.docs ) for offset in self._textual_finder.find_offsets(tools.source_code): occurrence = Occurrence(tools, offset) for filter in self.filters: result = filter(occurrence) if result is None: continue if result: yield occurrence break def create_finder( project, name, pyname, only_calls=False, imports=True, unsure=None, docs=False, instance=None, in_hierarchy=False, keywords=True, ): """A factory for `Finder` Based on the arguments it creates a list of filters. `instance` argument is needed only when you want implicit interfaces to be considered. """ pynames_ = set([pyname]) filters = [] if only_calls: filters.append(CallsFilter()) if not imports: filters.append(NoImportsFilter()) if not keywords: filters.append(NoKeywordsFilter()) if isinstance(instance, pynames.ParameterName): for pyobject in instance.get_objects(): try: pynames_.add(pyobject[name]) except exceptions.AttributeNotFoundError: pass for pyname in pynames_: filters.append(PyNameFilter(pyname)) if in_hierarchy: filters.append(InHierarchyFilter(pyname)) if unsure: filters.append(UnsureFilter(unsure)) return Finder(project, name, filters=filters, docs=docs) class Occurrence(object): def __init__(self, tools, offset): self.tools = tools self.offset = offset self.resource = tools.resource @utils.saveit def get_word_range(self): return self.tools.word_finder.get_word_range(self.offset) @utils.saveit def get_primary_range(self): return self.tools.word_finder.get_primary_range(self.offset) @utils.saveit def get_pyname(self): try: return self.tools.name_finder.get_pyname_at(self.offset) except exceptions.BadIdentifierError: pass @utils.saveit def get_primary_and_pyname(self): try: return self.tools.name_finder.get_primary_and_pyname_at(self.offset) except exceptions.BadIdentifierError: pass @utils.saveit def is_in_import_statement(self): return self.tools.word_finder.is_from_statement( self.offset ) or self.tools.word_finder.is_import_statement(self.offset) def is_called(self): return self.tools.word_finder.is_a_function_being_called(self.offset) def is_defined(self): return self.tools.word_finder.is_a_class_or_function_name_in_header(self.offset) def is_a_fixed_primary(self): return self.tools.word_finder.is_a_class_or_function_name_in_header( self.offset ) or self.tools.word_finder.is_a_name_after_from_import(self.offset) def is_written(self): return self.tools.word_finder.is_assigned_here(self.offset) def is_unsure(self): return unsure_pyname(self.get_pyname()) def is_function_keyword_parameter(self): return self.tools.word_finder.is_function_keyword_parameter(self.offset) @property @utils.saveit def lineno(self): offset = self.get_word_range()[0] return self.tools.pymodule.lines.get_line_number(offset) def same_pyname(expected, pyname): """Check whether `expected` and `pyname` are the same""" if expected is None or pyname is None: return False if expected == pyname: return True if type(expected) not in (pynames.ImportedModule, pynames.ImportedName) and type( pyname ) not in (pynames.ImportedModule, pynames.ImportedName): return False return ( expected.get_definition_location() == pyname.get_definition_location() and expected.get_object() == pyname.get_object() ) def unsure_pyname(pyname, unbound=True): """Return `True` if we don't know what this name references""" if pyname is None: return True if unbound and not isinstance(pyname, pynames.UnboundName): return False if pyname.get_object() == pyobjects.get_unknown(): return True class PyNameFilter(object): """For finding occurrences of a name.""" def __init__(self, pyname): self.pyname = pyname def __call__(self, occurrence): if same_pyname(self.pyname, occurrence.get_pyname()): return True class InHierarchyFilter(object): """Finds the occurrence if the name is in the class's hierarchy.""" def __init__(self, pyname, implementations_only=False): self.pyname = pyname self.impl_only = implementations_only self.pyclass = self._get_containing_class(pyname) if self.pyclass is not None: self.name = pyname.get_object().get_name() self.roots = self._get_root_classes(self.pyclass, self.name) else: self.roots = None def __call__(self, occurrence): if self.roots is None: return pyclass = self._get_containing_class(occurrence.get_pyname()) if pyclass is not None: roots = self._get_root_classes(pyclass, self.name) if self.roots.intersection(roots): return True def _get_containing_class(self, pyname): if isinstance(pyname, pynames.DefinedName): scope = pyname.get_object().get_scope() parent = scope.parent if parent is not None and parent.get_kind() == "Class": return parent.pyobject def _get_root_classes(self, pyclass, name): if self.impl_only and pyclass == self.pyclass: return set([pyclass]) result = set() for superclass in pyclass.get_superclasses(): if name in superclass: result.update(self._get_root_classes(superclass, name)) if not result: return set([pyclass]) return result class UnsureFilter(object): """Occurrences where we don't knoow what the name references.""" def __init__(self, unsure): self.unsure = unsure def __call__(self, occurrence): if occurrence.is_unsure() and self.unsure(occurrence): return True class NoImportsFilter(object): """Don't include import statements as occurrences.""" def __call__(self, occurrence): if occurrence.is_in_import_statement(): return False class CallsFilter(object): """Filter out non-call occurrences.""" def __call__(self, occurrence): if not occurrence.is_called(): return False class NoKeywordsFilter(object): """Filter out keyword parameters.""" def __call__(self, occurrence): if occurrence.is_function_keyword_parameter(): return False class _TextualFinder(object): def __init__(self, name, docs=False): self.name = name self.docs = docs self.comment_pattern = _TextualFinder.any("comment", [r"#[^\n]*"]) self.string_pattern = _TextualFinder.any( "string", [codeanalyze.get_string_pattern()] ) self.f_string_pattern = _TextualFinder.any( "fstring", [codeanalyze.get_formatted_string_pattern()] ) self.pattern = self._get_occurrence_pattern(self.name) def find_offsets(self, source): if not self._fast_file_query(source): return if self.docs: searcher = self._normal_search else: searcher = self._re_search for matched in searcher(source): yield matched def _re_search(self, source): for match in self.pattern.finditer(source): if match.groupdict()["occurrence"]: yield match.start("occurrence") elif utils.pycompat.PY36 and match.groupdict()["fstring"]: f_string = match.groupdict()["fstring"] for occurrence_node in self._search_in_f_string(f_string): yield match.start("fstring") + occurrence_node.col_offset def _search_in_f_string(self, f_string): tree = ast.parse(f_string) for node in ast.walk(tree): if isinstance(node, ast.Name) and node.id == self.name: yield node def _normal_search(self, source): current = 0 while True: try: found = source.index(self.name, current) current = found + len(self.name) if (found == 0 or not self._is_id_char(source[found - 1])) and ( current == len(source) or not self._is_id_char(source[current]) ): yield found except ValueError: break def _is_id_char(self, c): return c.isalnum() or c == "_" def _fast_file_query(self, source): try: source.index(self.name) return True except ValueError: return False def _get_source(self, resource, pymodule): if resource is not None: return resource.read() else: return pymodule.source_code def _get_occurrence_pattern(self, name): occurrence_pattern = _TextualFinder.any("occurrence", ["\\b" + name + "\\b"]) pattern = re.compile( occurrence_pattern + "|" + self.comment_pattern + "|" + self.string_pattern + "|" + self.f_string_pattern ) return pattern @staticmethod def any(name, list_): return "(?P<%s>" % name + "|".join(list_) + ")" class _OccurrenceToolsCreator(object): def __init__(self, project, resource=None, pymodule=None, docs=False): self.project = project self.__resource = resource self.__pymodule = pymodule self.docs = docs @property @utils.saveit def name_finder(self): return evaluate.ScopeNameFinder(self.pymodule) @property @utils.saveit def source_code(self): return self.pymodule.source_code @property @utils.saveit def word_finder(self): return worder.Worder(self.source_code, self.docs) @property @utils.saveit def resource(self): if self.__resource is not None: return self.__resource if self.__pymodule is not None: return self.__pymodule.resource @property @utils.saveit def pymodule(self): if self.__pymodule is not None: return self.__pymodule return self.project.get_pymodule(self.resource)