"""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 predicate 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 contextlib import re from typing import Iterator from rope.base import ( ast, codeanalyze, evaluate, exceptions, pynames, pyobjects, utils, worder, ) class Finder: """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=None, docs=False): if filters is None: filters = [lambda o: True] 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_ = {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: 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): with contextlib.suppress(exceptions.BadIdentifierError): return self.tools.name_finder.get_pyname_at(self.offset) @utils.saveit def get_primary_and_pyname(self): with contextlib.suppress(exceptions.BadIdentifierError): return self.tools.name_finder.get_primary_and_pyname_at(self.offset) @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 not isinstance( expected, (pynames.ImportedModule, pynames.ImportedName), ) and not isinstance( pyname, (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: """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: """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 {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 {pyclass} return result class UnsureFilter: """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: """Don't include import statements as occurrences.""" def __call__(self, occurrence): if occurrence.is_in_import_statement(): return False class CallsFilter: """Filter out non-call occurrences.""" def __call__(self, occurrence): if not occurrence.is_called(): return False class NoKeywordsFilter: """Filter out keyword parameters.""" def __call__(self, occurrence): if occurrence.is_function_keyword_parameter(): return False class _TextualFinder: 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: str) -> Iterator[int]: if not self._fast_file_query(source): return if self.docs: searcher = self._normal_search else: searcher = self._re_search yield from searcher(source) def _re_search(self, source: str) -> Iterator[int]: for match in self.pattern.finditer(source): if match.groupdict()["occurrence"]: yield match.start("occurrence") elif match.groupdict()["fstring"]: f_string = match.groupdict()["fstring"] for offset in self._search_in_f_string(f_string): yield match.start("fstring") + offset def _search_in_f_string(self, f_string: str) -> Iterator[int]: tree = ast.parse(f_string) for node in ast.walk(tree): if isinstance(node, ast.Name) and node.id == self.name: yield node.col_offset elif isinstance(node, ast.Attribute) and node.attr == self.name: assert node.end_col_offset is not None yield node.end_col_offset - len(self.name) def _normal_search(self, source: str) -> Iterator[int]: 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): return self.name in source 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: 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)