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