Format: text/x-python
⬇ Download File (11.1 KB)
   1  import os
   2  import re
   3  
   4  from parso import python_bytes_to_unicode
   5  
   6  from jedi.debug import dbg
   7  from jedi.file_io import KnownContentFileIO, FolderIO
   8  from jedi.inference.names import SubModuleName
   9  from jedi.inference.imports import load_module_from_path
  10  from jedi.inference.filters import ParserTreeFilter
  11  from jedi.inference.gradual.conversion import convert_names
  12  
  13  _IGNORE_FOLDERS = ('.tox', '.venv', '.mypy_cache', 'venv', '__pycache__')
  14  
  15  _OPENED_FILE_LIMIT = 2000
  16  """
  17  Stats from a 2016 Lenovo Notebook running Linux:
  18  With os.walk, it takes about 10s to scan 11'000 files (without filesystem
  19  caching). Once cached it only takes 5s. So it is expected that reading all
  20  those files might take a few seconds, but not a lot more.
  21  """
  22  _PARSED_FILE_LIMIT = 30
  23  """
  24  For now we keep the amount of parsed files really low, since parsing might take
  25  easily 100ms for bigger files.
  26  """
  27  
  28  
  29  def _resolve_names(definition_names, avoid_names=()):
  30      for name in definition_names:
  31          if name in avoid_names:
  32              # Avoiding recursions here, because goto on a module name lands
  33              # on the same module.
  34              continue
  35  
  36          if not isinstance(name, SubModuleName):
  37              # SubModuleNames are not actually existing names but created
  38              # names when importing something like `import foo.bar.baz`.
  39              yield name
  40  
  41          if name.api_type == 'module':
  42              yield from _resolve_names(name.goto(), definition_names)
  43  
  44  
  45  def _dictionarize(names):
  46      return dict(
  47          (n if n.tree_name is None else n.tree_name, n)
  48          for n in names
  49      )
  50  
  51  
  52  def _find_defining_names(module_context, tree_name):
  53      found_names = _find_names(module_context, tree_name)
  54  
  55      for name in list(found_names):
  56          # Convert from/to stubs, because those might also be usages.
  57          found_names |= set(convert_names(
  58              [name],
  59              only_stubs=not name.get_root_context().is_stub(),
  60              prefer_stub_to_compiled=False
  61          ))
  62  
  63      found_names |= set(_find_global_variables(found_names, tree_name.value))
  64      for name in list(found_names):
  65          if name.api_type == 'param' or name.tree_name is None \
  66                  or name.tree_name.parent.type == 'trailer':
  67              continue
  68          found_names |= set(_add_names_in_same_context(name.parent_context, name.string_name))
  69      return set(_resolve_names(found_names))
  70  
  71  
  72  def _find_names(module_context, tree_name):
  73      name = module_context.create_name(tree_name)
  74      found_names = set(name.goto())
  75      found_names.add(name)
  76  
  77      return set(_resolve_names(found_names))
  78  
  79  
  80  def _add_names_in_same_context(context, string_name):
  81      if context.tree_node is None:
  82          return
  83  
  84      until_position = None
  85      while True:
  86          filter_ = ParserTreeFilter(
  87              parent_context=context,
  88              until_position=until_position,
  89          )
  90          names = set(filter_.get(string_name))
  91          if not names:
  92              break
  93          yield from names
  94          ordered = sorted(names, key=lambda x: x.start_pos)
  95          until_position = ordered[0].start_pos
  96  
  97  
  98  def _find_global_variables(names, search_name):
  99      for name in names:
 100          if name.tree_name is None:
 101              continue
 102          module_context = name.get_root_context()
 103          try:
 104              method = module_context.get_global_filter
 105          except AttributeError:
 106              continue
 107          else:
 108              for global_name in method().get(search_name):
 109                  yield global_name
 110                  c = module_context.create_context(global_name.tree_name)
 111                  yield from _add_names_in_same_context(c, global_name.string_name)
 112  
 113  
 114  def find_references(module_context, tree_name, only_in_module=False):
 115      inf = module_context.inference_state
 116      search_name = tree_name.value
 117  
 118      # We disable flow analysis, because if we have ifs that are only true in
 119      # certain cases, we want both sides.
 120      try:
 121          inf.flow_analysis_enabled = False
 122          found_names = _find_defining_names(module_context, tree_name)
 123      finally:
 124          inf.flow_analysis_enabled = True
 125  
 126      found_names_dct = _dictionarize(found_names)
 127  
 128      module_contexts = [module_context]
 129      if not only_in_module:
 130          for m in set(d.get_root_context() for d in found_names):
 131              if m != module_context and m.tree_node is not None \
 132                      and inf.project.path in m.py__file__().parents:
 133                  module_contexts.append(m)
 134      # For param no search for other modules is necessary.
 135      if only_in_module or any(n.api_type == 'param' for n in found_names):
 136          potential_modules = module_contexts
 137      else:
 138          potential_modules = get_module_contexts_containing_name(
 139              inf,
 140              module_contexts,
 141              search_name,
 142          )
 143  
 144      non_matching_reference_maps = {}
 145      for module_context in potential_modules:
 146          for name_leaf in module_context.tree_node.get_used_names().get(search_name, []):
 147              new = _dictionarize(_find_names(module_context, name_leaf))
 148              if any(tree_name in found_names_dct for tree_name in new):
 149                  found_names_dct.update(new)
 150                  for tree_name in new:
 151                      for dct in non_matching_reference_maps.get(tree_name, []):
 152                          # A reference that was previously searched for matches
 153                          # with a now found name. Merge.
 154                          found_names_dct.update(dct)
 155                      try:
 156                          del non_matching_reference_maps[tree_name]
 157                      except KeyError:
 158                          pass
 159              else:
 160                  for name in new:
 161                      non_matching_reference_maps.setdefault(name, []).append(new)
 162      result = found_names_dct.values()
 163      if only_in_module:
 164          return [n for n in result if n.get_root_context() == module_context]
 165      return result
 166  
 167  
 168  def _check_fs(inference_state, file_io, regex):
 169      try:
 170          code = file_io.read()
 171      except FileNotFoundError:
 172          return None
 173      code = python_bytes_to_unicode(code, errors='replace')
 174      if not regex.search(code):
 175          return None
 176      new_file_io = KnownContentFileIO(file_io.path, code)
 177      m = load_module_from_path(inference_state, new_file_io)
 178      if m.is_compiled():
 179          return None
 180      return m.as_context()
 181  
 182  
 183  def gitignored_paths(folder_io, file_io):
 184      ignored_paths_abs = set()
 185      ignored_paths_rel = set()
 186  
 187      for l in file_io.read().splitlines():
 188          if not l or l.startswith(b'#') or l.startswith(b'!') or b'*' in l:
 189              continue
 190  
 191          p = l.decode('utf-8', 'ignore').rstrip('/')
 192          if '/' in p:
 193              name = p.lstrip('/')
 194              ignored_paths_abs.add(os.path.join(folder_io.path, name))
 195          else:
 196              name = p
 197              ignored_paths_rel.add((folder_io.path, name))
 198  
 199      return ignored_paths_abs, ignored_paths_rel
 200  
 201  
 202  def expand_relative_ignore_paths(folder_io, relative_paths):
 203      curr_path = folder_io.path
 204      return {os.path.join(curr_path, p[1]) for p in relative_paths if curr_path.startswith(p[0])}
 205  
 206  
 207  def recurse_find_python_folders_and_files(folder_io, except_paths=()):
 208      except_paths = set(except_paths)
 209      except_paths_relative = set()
 210  
 211      for root_folder_io, folder_ios, file_ios in folder_io.walk():
 212          # Delete folders that we don't want to iterate over.
 213          for file_io in file_ios:
 214              path = file_io.path
 215              if path.suffix in ('.py', '.pyi'):
 216                  if path not in except_paths:
 217                      yield None, file_io
 218  
 219              if path.name == '.gitignore':
 220                  ignored_paths_abs, ignored_paths_rel = gitignored_paths(
 221                      root_folder_io, file_io
 222                  )
 223                  except_paths |= ignored_paths_abs
 224                  except_paths_relative |= ignored_paths_rel
 225  
 226          except_paths_relative_expanded = expand_relative_ignore_paths(
 227              root_folder_io, except_paths_relative
 228          )
 229  
 230          folder_ios[:] = [
 231              folder_io
 232              for folder_io in folder_ios
 233              if folder_io.path not in except_paths
 234              and folder_io.path not in except_paths_relative_expanded
 235              and folder_io.get_base_name() not in _IGNORE_FOLDERS
 236          ]
 237          for folder_io in folder_ios:
 238              yield folder_io, None
 239  
 240  
 241  def recurse_find_python_files(folder_io, except_paths=()):
 242      for folder_io, file_io in recurse_find_python_folders_and_files(folder_io, except_paths):
 243          if file_io is not None:
 244              yield file_io
 245  
 246  
 247  def _find_python_files_in_sys_path(inference_state, module_contexts):
 248      sys_path = inference_state.get_sys_path()
 249      except_paths = set()
 250      yielded_paths = [m.py__file__() for m in module_contexts]
 251      for module_context in module_contexts:
 252          file_io = module_context.get_value().file_io
 253          if file_io is None:
 254              continue
 255  
 256          folder_io = file_io.get_parent_folder()
 257          while True:
 258              path = folder_io.path
 259              if not any(path.startswith(p) for p in sys_path) or path in except_paths:
 260                  break
 261              for file_io in recurse_find_python_files(folder_io, except_paths):
 262                  if file_io.path not in yielded_paths:
 263                      yield file_io
 264              except_paths.add(path)
 265              folder_io = folder_io.get_parent_folder()
 266  
 267  
 268  def _find_project_modules(inference_state, module_contexts):
 269      except_ = [m.py__file__() for m in module_contexts]
 270      yield from recurse_find_python_files(FolderIO(inference_state.project.path), except_)
 271  
 272  
 273  def get_module_contexts_containing_name(inference_state, module_contexts, name,
 274                                          limit_reduction=1):
 275      """
 276      Search a name in the directories of modules.
 277  
 278      :param limit_reduction: Divides the limits on opening/parsing files by this
 279          factor.
 280      """
 281      # Skip non python modules
 282      for module_context in module_contexts:
 283          if module_context.is_compiled():
 284              continue
 285          yield module_context
 286  
 287      # Very short names are not searched in other modules for now to avoid lots
 288      # of file lookups.
 289      if len(name) <= 2:
 290          return
 291  
 292      # Currently not used, because there's only `scope=project` and `scope=file`
 293      # At the moment there is no such thing as `scope=sys.path`.
 294      # file_io_iterator = _find_python_files_in_sys_path(inference_state, module_contexts)
 295      file_io_iterator = _find_project_modules(inference_state, module_contexts)
 296      yield from search_in_file_ios(inference_state, file_io_iterator, name,
 297                                    limit_reduction=limit_reduction)
 298  
 299  
 300  def search_in_file_ios(inference_state, file_io_iterator, name,
 301                         limit_reduction=1, complete=False):
 302      parse_limit = _PARSED_FILE_LIMIT / limit_reduction
 303      open_limit = _OPENED_FILE_LIMIT / limit_reduction
 304      file_io_count = 0
 305      parsed_file_count = 0
 306      regex = re.compile(r'\b' + re.escape(name) + (r'' if complete else r'\b'))
 307      for file_io in file_io_iterator:
 308          file_io_count += 1
 309          m = _check_fs(inference_state, file_io, regex)
 310          if m is not None:
 311              parsed_file_count += 1
 312              yield m
 313              if parsed_file_count >= parse_limit:
 314                  dbg('Hit limit of parsed files: %s', parse_limit)
 315                  break
 316  
 317          if file_io_count >= open_limit:
 318              dbg('Hit limit of opened files: %s', open_limit)
 319              break
 320