Repository: palantir/python-language-server
Pull request #846 (2020-08-25 02:39:31)

Correct method and attribute detection on document/symbols call
This PR corrects the types of methods and attributes, which were marked as functions and variables by Jedi, respectively


Patch:
diff --git a/pyls/_utils.py b/pyls/_utils.py
index b1a3bd9..9163bfc 100644
--- a/pyls/_utils.py
+++ b/pyls/_utils.py
@@ -82,6 +82,18 @@ def find_parents(root, path, names):
     return []
 
 
+def path_to_dot_name(path):
+    """Given a path to a module, derive its dot-separated full name."""
+    directory = os.path.dirname(path)
+    module_name, _ = os.path.splitext(os.path.basename(path))
+    full_name = [module_name]
+    while os.path.exists(os.path.join(directory, '__init__.py')):
+        this_directory = os.path.basename(directory)
+        directory = os.path.dirname(directory)
+        full_name = [this_directory] + full_name
+    return '.'.join(full_name)
+
+
 def match_uri_to_workspace(uri, workspaces):
     if uri is None:
         return None
diff --git a/pyls/plugins/symbols.py b/pyls/plugins/symbols.py
index ced9721..0acf629 100644
--- a/pyls/plugins/symbols.py
+++ b/pyls/plugins/symbols.py
@@ -8,17 +8,65 @@ log = logging.getLogger(__name__)
 
 @hookimpl
 def pyls_document_symbols(config, document):
-    all_scopes = config.plugin_settings('jedi_symbols').get('all_scopes', True)
+    # pylint: disable=broad-except
+    # pylint: disable=too-many-nested-blocks
+    # pylint: disable=too-many-locals
+    # pylint: disable=too-many-branches
+    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)
-    return [{
-        'name': d.name,
-        'containerName': _container(d),
-        'location': {
-            'uri': document.uri,
-            'range': _range(d),
-        },
-        'kind': _kind(d),
-    } for d in definitions if _include_def(d)]
+    module_name = document.dot_path
+    symbols = []
+    exclude = set({})
+    redefinitions = {}
+    while definitions != []:
+        d = definitions.pop(0)
+        if not add_import_symbols:
+            sym_full_name = d.full_name
+            if sym_full_name is not None:
+                if (not sym_full_name.startswith(module_name) and
+                        not sym_full_name.startswith('__main__')):
+                    continue
+
+        if _include_def(d) and document.path == d.module_path:
+            tuple_range = _tuple_range(d)
+            if tuple_range in exclude:
+                continue
+
+            kind = redefinitions.get(tuple_range, None)
+            if kind is not None:
+                exclude |= {tuple_range}
+
+            if d.type == 'statement':
+                if d.description.startswith('self'):
+                    kind = 'field'
+
+            symbol = {
+                'name': d.name,
+                'containerName': _container(d),
+                'location': {
+                    'uri': document.uri,
+                    'range': _range(d),
+                },
+                'kind': _kind(d) if kind is None else _SYMBOL_KIND_MAP[kind],
+            }
+            symbols.append(symbol)
+
+            if d.type == 'class':
+                try:
+                    defined_names = list(d.defined_names())
+                    for method in defined_names:
+                        if method.type == 'function':
+                            redefinitions[_tuple_range(method)] = 'method'
+                        elif method.type == 'statement':
+                            redefinitions[_tuple_range(method)] = 'field'
+                        else:
+                            redefinitions[_tuple_range(method)] = method.type
+                    definitions = list(defined_names) + definitions
+                except Exception:
+                    pass
+    return symbols
 
 
 def _include_def(definition):
@@ -56,6 +104,11 @@ def _range(definition):
     }
 
 
+def _tuple_range(definition):
+    definition = definition._name.tree_name.get_definition()
+    return (definition.start_pos, definition.end_pos)
+
+
 _SYMBOL_KIND_MAP = {
     'none': SymbolKind.Variable,
     'type': SymbolKind.Class,
@@ -95,6 +148,7 @@ _SYMBOL_KIND_MAP = {
     'string': SymbolKind.String,
     'unicode': SymbolKind.String,
     'list': SymbolKind.Array,
+    'field': SymbolKind.Field
 }
 
 
diff --git a/pyls/workspace.py b/pyls/workspace.py
index 955b56c..7d5b95e 100644
--- a/pyls/workspace.py
+++ b/pyls/workspace.py
@@ -122,6 +122,7 @@ class Document(object):
         self.uri = uri
         self.version = version
         self.path = uris.to_fs_path(uri)
+        self.dot_path = _utils.path_to_dot_name(self.path)
         self.filename = os.path.basename(self.path)
 
         self._config = workspace._config


Test patch:
diff --git a/test/plugins/test_symbols.py b/test/plugins/test_symbols.py
index b80c0d6..a03c1fc 100644
--- a/test/plugins/test_symbols.py
+++ b/test/plugins/test_symbols.py
@@ -40,7 +40,7 @@ def helper_check_symbols_all_scope(symbols):
     # Check we have some sane mappings to VSCode constants
     assert sym('a')['kind'] == SymbolKind.Variable
     assert sym('B')['kind'] == SymbolKind.Class
-    assert sym('__init__')['kind'] == SymbolKind.Function
+    assert sym('__init__')['kind'] == SymbolKind.Method
     assert sym('main')['kind'] == SymbolKind.Function
 
     # Not going to get too in-depth here else we're just testing Jedi
@@ -54,7 +54,7 @@ def test_symbols(config, workspace):
 
     # All four symbols (import sys, a, B, main)
     # y is not in the root scope, it shouldn't be returned
-    assert len(symbols) == 4
+    assert len(symbols) == 5
 
     def sym(name):
         return [s for s in symbols if s['name'] == name][0]