Repository: QuantStack/pyls-memestra (language: python)

Issue:
Use `DiagnosticTag.Deprecated`
LSP 3.15 enables special treatment of diagnostics for deprecated code using [`DiagnosticTag`](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnosticTag)s. By adding `"tags": [2]` you can tell the clients that the code is deprecated and the client can decorate it with strike-through. I am in a bit of a chicken and egg problem writing tests for this feature for JupyterLab-LSP (https://github.com/jupyter-lsp/jupyterlab-lsp/pull/736) because most servers do not provide it yet; I thought it might be a good idea to implement if in pylsp-memestra early and test using it - what do you think?


Discussion / hints:
That sounds like a great suggestion.

Fix patch:
diff --git a/pyls_memestra/plugin.py b/pyls_memestra/plugin.py
index 4beda49..349386f 100644
--- a/pyls_memestra/plugin.py
+++ b/pyls_memestra/plugin.py
@@ -6,6 +6,11 @@
 logging.basicConfig(level=logging.ERROR)
 logger = logging.getLogger(__name__)
 
+# TODO: use lsp.DiagnosticTag once https://github.com/python-lsp/python-lsp-server/pull/142 is merged
+class DiagnosticTag:
+    Unnecessary = 1
+    Deprecated = 2
+
 @hookimpl
 def pylsp_settings():
     return {
@@ -30,6 +35,14 @@ def pylsp_lint(config, document):
     diagnostics = []
     search_paths = [os.path.dirname(os.path.abspath(document.path))]
     search_paths.extend(settings.get('additional_search_paths'))
+    supported_tags = set(
+        config.capabilities
+        .get('textDocument', {})
+        .get('publishDiagnostics', {})
+        .get('tagSupport', {})
+        .get('valueSet', set())
+    )
+
     try:
         with open(document.path, 'r') as code:
             deprecated_uses = memestra(
@@ -40,30 +53,34 @@ def pylsp_lint(config, document):
                 recursive=settings.get('recursive'),
                 cache_dir=settings.get('cache_dir'),
                 search_paths=search_paths)
-            diagnostics = format_text(deprecated_uses, diagnostics)
+            diagnostics = format_text(deprecated_uses, diagnostics, supported_tags)
     except SyntaxError as e:
         logger.error('Syntax error at {} - {} ({})', e.line, e.column, e.message)
         raise e
     return diagnostics
 
-def format_text(deprecated_uses, diagnostics):
+def format_text(deprecated_uses, diagnostics, supported_tags=None):
+    if supported_tags is None:
+        supported_tags = set()
+    tags = [DiagnosticTag.Deprecated]
     for fname, fd, lineno, colno, reason in deprecated_uses:
         err_range = {
             'start': {'line': lineno - 1, 'character': colno},
             'end': {'line': lineno - 1, 'character': colno + len(fname)},
         }
         if reason and reason != "reason":
-            diagnostics.append({
-                'source': 'memestra',
-                'range': err_range,
-                'message': fname + " is deprecated. " + reason,
-                'severity': lsp.DiagnosticSeverity.Information,
-            })
+            message = fname + " is deprecated. " + reason
         else:
-            diagnostics.append({
-                'source': 'memestra',
-                'range': err_range,
-                'message': fname + " is deprecated.",
-                'severity': lsp.DiagnosticSeverity.Information,
-            })
+            message = fname + " is deprecated."
+
+        diagnostic = {
+            'source': 'memestra',
+            'range': err_range,
+            'message': message,
+            'severity': lsp.DiagnosticSeverity.Information,
+        }
+
+        if DiagnosticTag.Deprecated in supported_tags:
+            diagnostic['tags'] = tags
+        diagnostics.append(diagnostic)
     return diagnostics


Test patch:
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 398556f..d566d3b 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -40,13 +40,13 @@ def write_doc(text):
     yield write_doc
     os.remove(temp_file.name)
 
-def build_diagnostic(name, start, end, reason, source="memestra", severity=3):
+def build_diagnostic(name, start, end, reason, source="memestra", severity=3, tags=None):
     if reason is None:
         message = name + " is deprecated."
     else:
         message = name + " is deprecated. " + reason
 
-    return {
+    diagnostic = {
             "source": source,
             "range": {
                 "start": {
@@ -59,8 +59,11 @@ def build_diagnostic(name, start, end, reason, source="memestra", severity=3):
                 }
             },
             "message": message,
-            "severity": severity
+            "severity": severity,
         }
+    if tags is not None:
+        diagnostic["tags"] = tags
+    return diagnostic
 
 def update_setting(config, name, value):
     settings = deepcopy(config._settings)
@@ -76,6 +79,31 @@ def test_basic(workspace, config):
         build_diagnostic("imported", (9, 0), (9, 8), "test reason"),
     ]
 
+
+def test_tag_support(workspace, config):
+    doc = Document(uris.from_fs_path(str(data / "file.py")), workspace)
+    config.capabilities['textDocument'] = {
+        'publishDiagnostics': {'tagSupport': {'valueSet': [2]}}
+    }
+    diagnostics = pylsp_lint(config, doc)
+
+    assert diagnostics == [
+        build_diagnostic("foo", (7, 4), (7, 7), "deprecated at some point", tags=[2]),
+        build_diagnostic("imported", (9, 0), (9, 8), "test reason", tags=[2]),
+    ]
+
+def test_tag_absent(workspace, config):
+    doc = Document(uris.from_fs_path(str(data / "file.py")), workspace)
+    config.capabilities['textDocument'] = {
+        'publishDiagnostics': {'tagSupport': {'valueSet': [1]}}
+    }
+    diagnostics = pylsp_lint(config, doc)
+
+    assert diagnostics == [
+        build_diagnostic("foo", (7, 4), (7, 7), "deprecated at some point", tags=None),
+        build_diagnostic("imported", (9, 0), (9, 8), "test reason", tags=None),
+    ]
+
 def test_decorator_name(workspace, config, document):
     doc = document("""
 import bogus