Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve how we determine if a symbol was imported from other libraries #71

Merged
merged 3 commits into from
Aug 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions pylsp/plugins/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ def pylsp_document_symbols(config, document):
# pylint: disable=too-many-nested-blocks
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements

symbols_settings = config.plugin_settings('jedi_symbols')
all_scopes = symbols_settings.get('all_scopes', True)
add_import_symbols = symbols_settings.get('include_import_symbols', True)
definitions = document.jedi_names(all_scopes=all_scopes)
symbols = []
exclude = set({})
redefinitions = {}

while definitions != []:
d = definitions.pop(0)

Expand All @@ -33,27 +36,47 @@ def pylsp_document_symbols(config, document):
if ' import ' in code or 'import ' in code:
continue

# Skip comparing module names.
# Skip imported symbols comparing module names.
sym_full_name = d.full_name
module_name = document.dot_path
document_dot_path = document.dot_path
if sym_full_name is not None:
# module_name returns where the symbol is imported, whereas
# full_name says where it really comes from. So if the parent
# modules in full_name are not in module_name, it means the
# symbol was not defined there.
# Note: The last element of sym_full_name is the symbol itself,
# so we don't need to use it below.
# We assume a symbol is imported from another module to start
# with.
imported_symbol = True
for mod in sym_full_name.split('.')[:-1]:
if mod in module_name:
imported_symbol = False

# The last element of sym_full_name is the symbol itself, so
# we need to discard it to do module comparisons below.
if '.' in sym_full_name:
sym_module_name = sym_full_name.rpartition('.')[0]

# This is necessary to display symbols in init files (the checks
# below fail without it).
if document_dot_path.endswith('__init__'):
document_dot_path = document_dot_path.rpartition('.')[0]

# document_dot_path is the module where the symbol is imported,
# whereas sym_module_name is the one where it was declared.
if sym_module_name.startswith(document_dot_path):
# If sym_module_name starts with the same string as document_dot_path,
# we can safely assume it was declared in the document.
imported_symbol = False
elif sym_module_name.split('.')[0] in document_dot_path.split('.'):
# If the first module in sym_module_name is one of the modules in
# document_dot_path, we need to check if sym_module_name starts
# with the modules in document_dot_path.
document_mods = document_dot_path.split('.')
for i in range(1, len(document_mods) + 1):
submod = '.'.join(document_mods[-i:])
if sym_module_name.startswith(submod):
imported_symbol = False
break

# When there's no __init__.py next to a file or in one of its
# parents, the check above fails. However, Jedi has a nice way
# parents, the checks above fail. However, Jedi has a nice way
# to tell if the symbol was declared in the same file: if
# full_name starts by __main__.
if imported_symbol:
if not sym_full_name.startswith('__main__'):
if not sym_module_name.startswith('__main__'):
continue

try:
Expand Down
2 changes: 2 additions & 0 deletions test/test_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def test_exit_with_parent_process_died(client_exited_server): # pylint: disable
assert not client_exited_server.client_thread.is_alive()


@flaky(max_runs=10, min_passes=1)
@pytest.mark.skipif(sys.platform.startswith('linux'), reason='Fails on linux')
def test_not_exit_without_check_parent_process_flag(client_server): # pylint: disable=redefined-outer-name
response = client_server._endpoint.request('initialize', {
Expand All @@ -112,6 +113,7 @@ def test_not_exit_without_check_parent_process_flag(client_server): # pylint: d
assert 'capabilities' in response


@flaky(max_runs=10, min_passes=1)
@pytest.mark.skipif(RUNNING_IN_CI, reason='This test is hanging on CI')
def test_missing_message(client_server): # pylint: disable=redefined-outer-name
with pytest.raises(JsonRpcMethodNotFound):
Expand Down