You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			320 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			320 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
import os
 | 
						|
import re
 | 
						|
 | 
						|
from parso import python_bytes_to_unicode
 | 
						|
 | 
						|
from jedi.debug import dbg
 | 
						|
from jedi.file_io import KnownContentFileIO, FolderIO
 | 
						|
from jedi.inference.names import SubModuleName
 | 
						|
from jedi.inference.imports import load_module_from_path
 | 
						|
from jedi.inference.filters import ParserTreeFilter
 | 
						|
from jedi.inference.gradual.conversion import convert_names
 | 
						|
 | 
						|
_IGNORE_FOLDERS = ('.tox', '.venv', '.mypy_cache', 'venv', '__pycache__')
 | 
						|
 | 
						|
_OPENED_FILE_LIMIT = 2000
 | 
						|
"""
 | 
						|
Stats from a 2016 Lenovo Notebook running Linux:
 | 
						|
With os.walk, it takes about 10s to scan 11'000 files (without filesystem
 | 
						|
caching). Once cached it only takes 5s. So it is expected that reading all
 | 
						|
those files might take a few seconds, but not a lot more.
 | 
						|
"""
 | 
						|
_PARSED_FILE_LIMIT = 30
 | 
						|
"""
 | 
						|
For now we keep the amount of parsed files really low, since parsing might take
 | 
						|
easily 100ms for bigger files.
 | 
						|
"""
 | 
						|
 | 
						|
 | 
						|
def _resolve_names(definition_names, avoid_names=()):
 | 
						|
    for name in definition_names:
 | 
						|
        if name in avoid_names:
 | 
						|
            # Avoiding recursions here, because goto on a module name lands
 | 
						|
            # on the same module.
 | 
						|
            continue
 | 
						|
 | 
						|
        if not isinstance(name, SubModuleName):
 | 
						|
            # SubModuleNames are not actually existing names but created
 | 
						|
            # names when importing something like `import foo.bar.baz`.
 | 
						|
            yield name
 | 
						|
 | 
						|
        if name.api_type == 'module':
 | 
						|
            yield from _resolve_names(name.goto(), definition_names)
 | 
						|
 | 
						|
 | 
						|
def _dictionarize(names):
 | 
						|
    return dict(
 | 
						|
        (n if n.tree_name is None else n.tree_name, n)
 | 
						|
        for n in names
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def _find_defining_names(module_context, tree_name):
 | 
						|
    found_names = _find_names(module_context, tree_name)
 | 
						|
 | 
						|
    for name in list(found_names):
 | 
						|
        # Convert from/to stubs, because those might also be usages.
 | 
						|
        found_names |= set(convert_names(
 | 
						|
            [name],
 | 
						|
            only_stubs=not name.get_root_context().is_stub(),
 | 
						|
            prefer_stub_to_compiled=False
 | 
						|
        ))
 | 
						|
 | 
						|
    found_names |= set(_find_global_variables(found_names, tree_name.value))
 | 
						|
    for name in list(found_names):
 | 
						|
        if name.api_type == 'param' or name.tree_name is None \
 | 
						|
                or name.tree_name.parent.type == 'trailer':
 | 
						|
            continue
 | 
						|
        found_names |= set(_add_names_in_same_context(name.parent_context, name.string_name))
 | 
						|
    return set(_resolve_names(found_names))
 | 
						|
 | 
						|
 | 
						|
def _find_names(module_context, tree_name):
 | 
						|
    name = module_context.create_name(tree_name)
 | 
						|
    found_names = set(name.goto())
 | 
						|
    found_names.add(name)
 | 
						|
 | 
						|
    return set(_resolve_names(found_names))
 | 
						|
 | 
						|
 | 
						|
def _add_names_in_same_context(context, string_name):
 | 
						|
    if context.tree_node is None:
 | 
						|
        return
 | 
						|
 | 
						|
    until_position = None
 | 
						|
    while True:
 | 
						|
        filter_ = ParserTreeFilter(
 | 
						|
            parent_context=context,
 | 
						|
            until_position=until_position,
 | 
						|
        )
 | 
						|
        names = set(filter_.get(string_name))
 | 
						|
        if not names:
 | 
						|
            break
 | 
						|
        yield from names
 | 
						|
        ordered = sorted(names, key=lambda x: x.start_pos)
 | 
						|
        until_position = ordered[0].start_pos
 | 
						|
 | 
						|
 | 
						|
def _find_global_variables(names, search_name):
 | 
						|
    for name in names:
 | 
						|
        if name.tree_name is None:
 | 
						|
            continue
 | 
						|
        module_context = name.get_root_context()
 | 
						|
        try:
 | 
						|
            method = module_context.get_global_filter
 | 
						|
        except AttributeError:
 | 
						|
            continue
 | 
						|
        else:
 | 
						|
            for global_name in method().get(search_name):
 | 
						|
                yield global_name
 | 
						|
                c = module_context.create_context(global_name.tree_name)
 | 
						|
                yield from _add_names_in_same_context(c, global_name.string_name)
 | 
						|
 | 
						|
 | 
						|
def find_references(module_context, tree_name, only_in_module=False):
 | 
						|
    inf = module_context.inference_state
 | 
						|
    search_name = tree_name.value
 | 
						|
 | 
						|
    # We disable flow analysis, because if we have ifs that are only true in
 | 
						|
    # certain cases, we want both sides.
 | 
						|
    try:
 | 
						|
        inf.flow_analysis_enabled = False
 | 
						|
        found_names = _find_defining_names(module_context, tree_name)
 | 
						|
    finally:
 | 
						|
        inf.flow_analysis_enabled = True
 | 
						|
 | 
						|
    found_names_dct = _dictionarize(found_names)
 | 
						|
 | 
						|
    module_contexts = [module_context]
 | 
						|
    if not only_in_module:
 | 
						|
        for m in set(d.get_root_context() for d in found_names):
 | 
						|
            if m != module_context and m.tree_node is not None \
 | 
						|
                    and inf.project.path in m.py__file__().parents:
 | 
						|
                module_contexts.append(m)
 | 
						|
    # For param no search for other modules is necessary.
 | 
						|
    if only_in_module or any(n.api_type == 'param' for n in found_names):
 | 
						|
        potential_modules = module_contexts
 | 
						|
    else:
 | 
						|
        potential_modules = get_module_contexts_containing_name(
 | 
						|
            inf,
 | 
						|
            module_contexts,
 | 
						|
            search_name,
 | 
						|
        )
 | 
						|
 | 
						|
    non_matching_reference_maps = {}
 | 
						|
    for module_context in potential_modules:
 | 
						|
        for name_leaf in module_context.tree_node.get_used_names().get(search_name, []):
 | 
						|
            new = _dictionarize(_find_names(module_context, name_leaf))
 | 
						|
            if any(tree_name in found_names_dct for tree_name in new):
 | 
						|
                found_names_dct.update(new)
 | 
						|
                for tree_name in new:
 | 
						|
                    for dct in non_matching_reference_maps.get(tree_name, []):
 | 
						|
                        # A reference that was previously searched for matches
 | 
						|
                        # with a now found name. Merge.
 | 
						|
                        found_names_dct.update(dct)
 | 
						|
                    try:
 | 
						|
                        del non_matching_reference_maps[tree_name]
 | 
						|
                    except KeyError:
 | 
						|
                        pass
 | 
						|
            else:
 | 
						|
                for name in new:
 | 
						|
                    non_matching_reference_maps.setdefault(name, []).append(new)
 | 
						|
    result = found_names_dct.values()
 | 
						|
    if only_in_module:
 | 
						|
        return [n for n in result if n.get_root_context() == module_context]
 | 
						|
    return result
 | 
						|
 | 
						|
 | 
						|
def _check_fs(inference_state, file_io, regex):
 | 
						|
    try:
 | 
						|
        code = file_io.read()
 | 
						|
    except FileNotFoundError:
 | 
						|
        return None
 | 
						|
    code = python_bytes_to_unicode(code, errors='replace')
 | 
						|
    if not regex.search(code):
 | 
						|
        return None
 | 
						|
    new_file_io = KnownContentFileIO(file_io.path, code)
 | 
						|
    m = load_module_from_path(inference_state, new_file_io)
 | 
						|
    if m.is_compiled():
 | 
						|
        return None
 | 
						|
    return m.as_context()
 | 
						|
 | 
						|
 | 
						|
def gitignored_paths(folder_io, file_io):
 | 
						|
    ignored_paths_abs = set()
 | 
						|
    ignored_paths_rel = set()
 | 
						|
 | 
						|
    for l in file_io.read().splitlines():
 | 
						|
        if not l or l.startswith(b'#') or l.startswith(b'!') or b'*' in l:
 | 
						|
            continue
 | 
						|
 | 
						|
        p = l.decode('utf-8', 'ignore').rstrip('/')
 | 
						|
        if '/' in p:
 | 
						|
            name = p.lstrip('/')
 | 
						|
            ignored_paths_abs.add(os.path.join(folder_io.path, name))
 | 
						|
        else:
 | 
						|
            name = p
 | 
						|
            ignored_paths_rel.add((folder_io.path, name))
 | 
						|
 | 
						|
    return ignored_paths_abs, ignored_paths_rel
 | 
						|
 | 
						|
 | 
						|
def expand_relative_ignore_paths(folder_io, relative_paths):
 | 
						|
    curr_path = folder_io.path
 | 
						|
    return {os.path.join(curr_path, p[1]) for p in relative_paths if curr_path.startswith(p[0])}
 | 
						|
 | 
						|
 | 
						|
def recurse_find_python_folders_and_files(folder_io, except_paths=()):
 | 
						|
    except_paths = set(except_paths)
 | 
						|
    except_paths_relative = set()
 | 
						|
 | 
						|
    for root_folder_io, folder_ios, file_ios in folder_io.walk():
 | 
						|
        # Delete folders that we don't want to iterate over.
 | 
						|
        for file_io in file_ios:
 | 
						|
            path = file_io.path
 | 
						|
            if path.suffix in ('.py', '.pyi'):
 | 
						|
                if path not in except_paths:
 | 
						|
                    yield None, file_io
 | 
						|
 | 
						|
            if path.name == '.gitignore':
 | 
						|
                ignored_paths_abs, ignored_paths_rel = gitignored_paths(
 | 
						|
                    root_folder_io, file_io
 | 
						|
                )
 | 
						|
                except_paths |= ignored_paths_abs
 | 
						|
                except_paths_relative |= ignored_paths_rel
 | 
						|
 | 
						|
        except_paths_relative_expanded = expand_relative_ignore_paths(
 | 
						|
            root_folder_io, except_paths_relative
 | 
						|
        )
 | 
						|
 | 
						|
        folder_ios[:] = [
 | 
						|
            folder_io
 | 
						|
            for folder_io in folder_ios
 | 
						|
            if folder_io.path not in except_paths
 | 
						|
            and folder_io.path not in except_paths_relative_expanded
 | 
						|
            and folder_io.get_base_name() not in _IGNORE_FOLDERS
 | 
						|
        ]
 | 
						|
        for folder_io in folder_ios:
 | 
						|
            yield folder_io, None
 | 
						|
 | 
						|
 | 
						|
def recurse_find_python_files(folder_io, except_paths=()):
 | 
						|
    for folder_io, file_io in recurse_find_python_folders_and_files(folder_io, except_paths):
 | 
						|
        if file_io is not None:
 | 
						|
            yield file_io
 | 
						|
 | 
						|
 | 
						|
def _find_python_files_in_sys_path(inference_state, module_contexts):
 | 
						|
    sys_path = inference_state.get_sys_path()
 | 
						|
    except_paths = set()
 | 
						|
    yielded_paths = [m.py__file__() for m in module_contexts]
 | 
						|
    for module_context in module_contexts:
 | 
						|
        file_io = module_context.get_value().file_io
 | 
						|
        if file_io is None:
 | 
						|
            continue
 | 
						|
 | 
						|
        folder_io = file_io.get_parent_folder()
 | 
						|
        while True:
 | 
						|
            path = folder_io.path
 | 
						|
            if not any(path.startswith(p) for p in sys_path) or path in except_paths:
 | 
						|
                break
 | 
						|
            for file_io in recurse_find_python_files(folder_io, except_paths):
 | 
						|
                if file_io.path not in yielded_paths:
 | 
						|
                    yield file_io
 | 
						|
            except_paths.add(path)
 | 
						|
            folder_io = folder_io.get_parent_folder()
 | 
						|
 | 
						|
 | 
						|
def _find_project_modules(inference_state, module_contexts):
 | 
						|
    except_ = [m.py__file__() for m in module_contexts]
 | 
						|
    yield from recurse_find_python_files(FolderIO(inference_state.project.path), except_)
 | 
						|
 | 
						|
 | 
						|
def get_module_contexts_containing_name(inference_state, module_contexts, name,
 | 
						|
                                        limit_reduction=1):
 | 
						|
    """
 | 
						|
    Search a name in the directories of modules.
 | 
						|
 | 
						|
    :param limit_reduction: Divides the limits on opening/parsing files by this
 | 
						|
        factor.
 | 
						|
    """
 | 
						|
    # Skip non python modules
 | 
						|
    for module_context in module_contexts:
 | 
						|
        if module_context.is_compiled():
 | 
						|
            continue
 | 
						|
        yield module_context
 | 
						|
 | 
						|
    # Very short names are not searched in other modules for now to avoid lots
 | 
						|
    # of file lookups.
 | 
						|
    if len(name) <= 2:
 | 
						|
        return
 | 
						|
 | 
						|
    # Currently not used, because there's only `scope=project` and `scope=file`
 | 
						|
    # At the moment there is no such thing as `scope=sys.path`.
 | 
						|
    # file_io_iterator = _find_python_files_in_sys_path(inference_state, module_contexts)
 | 
						|
    file_io_iterator = _find_project_modules(inference_state, module_contexts)
 | 
						|
    yield from search_in_file_ios(inference_state, file_io_iterator, name,
 | 
						|
                                  limit_reduction=limit_reduction)
 | 
						|
 | 
						|
 | 
						|
def search_in_file_ios(inference_state, file_io_iterator, name,
 | 
						|
                       limit_reduction=1, complete=False):
 | 
						|
    parse_limit = _PARSED_FILE_LIMIT / limit_reduction
 | 
						|
    open_limit = _OPENED_FILE_LIMIT / limit_reduction
 | 
						|
    file_io_count = 0
 | 
						|
    parsed_file_count = 0
 | 
						|
    regex = re.compile(r'\b' + re.escape(name) + (r'' if complete else r'\b'))
 | 
						|
    for file_io in file_io_iterator:
 | 
						|
        file_io_count += 1
 | 
						|
        m = _check_fs(inference_state, file_io, regex)
 | 
						|
        if m is not None:
 | 
						|
            parsed_file_count += 1
 | 
						|
            yield m
 | 
						|
            if parsed_file_count >= parse_limit:
 | 
						|
                dbg('Hit limit of parsed files: %s', parse_limit)
 | 
						|
                break
 | 
						|
 | 
						|
        if file_io_count >= open_limit:
 | 
						|
            dbg('Hit limit of opened files: %s', open_limit)
 | 
						|
            break
 |