java-functional-lsp 0.9.11__tar.gz → 0.9.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/PKG-INFO +32 -2
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/README.md +31 -1
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/pyproject.toml +1 -1
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/server.py +96 -2
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_server.py +464 -4
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.gitignore +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/LICENSE +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/SKILL.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/base.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/fixes.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/merkle.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/proxy.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/__init__.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/conftest.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_base.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_config.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_e2e_jdtls.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_functional_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_merkle.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_merkle_proxy.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_mutation_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_proxy.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/tests/test_suppress.py +0 -0
- {java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-functional-lsp
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.13
|
|
4
4
|
Summary: Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions
|
|
5
5
|
Project-URL: Homepage, https://github.com/aviadshiber/java-functional-lsp
|
|
6
6
|
Project-URL: Repository, https://github.com/aviadshiber/java-functional-lsp
|
|
@@ -270,7 +270,37 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
270
270
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
271
271
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
272
272
|
|
|
273
|
-
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
273
|
+
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
274
|
+
|
|
275
|
+
### jdtls settings
|
|
276
|
+
|
|
277
|
+
The server sends Maven/Gradle settings to jdtls at startup via `initializationOptions.settings`. Defaults are optimized for Maven monorepos (Maven enabled, Gradle disabled, build artifact exclusions). Override via `.java-functional-lsp.json`:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"jdtls": {
|
|
282
|
+
"settings": {
|
|
283
|
+
"java": {
|
|
284
|
+
"import": {
|
|
285
|
+
"maven": { "enabled": true },
|
|
286
|
+
"gradle": { "enabled": true },
|
|
287
|
+
"exclusions": ["**/node_modules/**", "**/target/**"]
|
|
288
|
+
},
|
|
289
|
+
"configuration": { "updateBuildConfiguration": "automatic" },
|
|
290
|
+
"maven": { "downloadSources": true }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Custom settings fully replace the defaults (no merge). See the [jdtls Preferences reference](https://github.com/eclipse-jdtls/eclipse.jdt.ls/blob/main/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java) for all available keys.
|
|
298
|
+
|
|
299
|
+
### jdtls cache
|
|
300
|
+
|
|
301
|
+
The jdtls Eclipse workspace index is cached in `~/.cache/jdtls-data/`. Warm starts (~10-20s) reuse this cache; cold starts (60-120s) rebuild from scratch. The cache is automatically invalidated when jdtls or Java is upgraded, but **not** when java-functional-lsp is upgraded — our Python code changes don't affect the Eclipse index.
|
|
302
|
+
|
|
303
|
+
To force a clean rebuild: `rm -rf ~/.cache/jdtls-data/`
|
|
274
304
|
|
|
275
305
|
### Suppressing jdtls diagnostics
|
|
276
306
|
|
|
@@ -242,7 +242,37 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
242
242
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
243
243
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
244
244
|
|
|
245
|
-
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
245
|
+
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
246
|
+
|
|
247
|
+
### jdtls settings
|
|
248
|
+
|
|
249
|
+
The server sends Maven/Gradle settings to jdtls at startup via `initializationOptions.settings`. Defaults are optimized for Maven monorepos (Maven enabled, Gradle disabled, build artifact exclusions). Override via `.java-functional-lsp.json`:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"jdtls": {
|
|
254
|
+
"settings": {
|
|
255
|
+
"java": {
|
|
256
|
+
"import": {
|
|
257
|
+
"maven": { "enabled": true },
|
|
258
|
+
"gradle": { "enabled": true },
|
|
259
|
+
"exclusions": ["**/node_modules/**", "**/target/**"]
|
|
260
|
+
},
|
|
261
|
+
"configuration": { "updateBuildConfiguration": "automatic" },
|
|
262
|
+
"maven": { "downloadSources": true }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Custom settings fully replace the defaults (no merge). See the [jdtls Preferences reference](https://github.com/eclipse-jdtls/eclipse.jdt.ls/blob/main/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java) for all available keys.
|
|
270
|
+
|
|
271
|
+
### jdtls cache
|
|
272
|
+
|
|
273
|
+
The jdtls Eclipse workspace index is cached in `~/.cache/jdtls-data/`. Warm starts (~10-20s) reuse this cache; cold starts (60-120s) rebuild from scratch. The cache is automatically invalidated when jdtls or Java is upgraded, but **not** when java-functional-lsp is upgraded — our Python code changes don't affect the Eclipse index.
|
|
274
|
+
|
|
275
|
+
To force a clean rebuild: `rm -rf ~/.cache/jdtls-data/`
|
|
246
276
|
|
|
247
277
|
### Suppressing jdtls diagnostics
|
|
248
278
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "java-functional-lsp"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.13"
|
|
8
8
|
description = "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -12,6 +12,8 @@ import logging
|
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
14
|
import sys
|
|
15
|
+
import time
|
|
16
|
+
from collections import OrderedDict
|
|
15
17
|
from collections.abc import Coroutine
|
|
16
18
|
from pathlib import Path
|
|
17
19
|
from typing import Any
|
|
@@ -19,7 +21,7 @@ from typing import Any
|
|
|
19
21
|
from lsprotocol import types as lsp
|
|
20
22
|
from lsprotocol.converters import get_converter
|
|
21
23
|
from pygls.lsp.server import LanguageServer
|
|
22
|
-
from pygls.uris import to_fs_path
|
|
24
|
+
from pygls.uris import from_fs_path, to_fs_path
|
|
23
25
|
|
|
24
26
|
from .analyzers.base import Analyzer, Severity, get_parser, is_excluded, is_suppressed
|
|
25
27
|
from .analyzers.base import Diagnostic as LintDiagnostic
|
|
@@ -61,6 +63,34 @@ _JDTLS_SKIP_CLIENTS = ("IntelliJ", "JetBrains")
|
|
|
61
63
|
#: go-to-definition, references, etc.
|
|
62
64
|
_converter = get_converter()
|
|
63
65
|
|
|
66
|
+
# Cap on _session_opened_uris: a session navigating many files still bounds memory.
|
|
67
|
+
# 4096 comfortably covers even aggressive monorepo navigation.
|
|
68
|
+
_MAX_OPENED_URIS = 4096
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _normalize_uri(uri: str) -> str:
|
|
72
|
+
"""Canonicalize a file:// URI so equivalent paths compare equal.
|
|
73
|
+
|
|
74
|
+
Round-trips through pygls.uris to normalize URL encoding differences
|
|
75
|
+
(``%20`` vs literal space, ``%3A`` vs ``:``, trailing slashes, double
|
|
76
|
+
slashes) between what the client sends and what jdtls echoes back.
|
|
77
|
+
|
|
78
|
+
Non-file URIs (``jdt://``, ``untitled://``, etc.) are returned unchanged —
|
|
79
|
+
they're never stored in ``_session_opened_uris`` so they'll fall through the gate
|
|
80
|
+
regardless. Case-insensitive filesystems (macOS HFS+, NTFS) are not
|
|
81
|
+
normalized here: attempting that would require ``Path.resolve()``, which
|
|
82
|
+
would stat the filesystem and follow symlinks — too expensive for a hot
|
|
83
|
+
path that runs on every jdtls publish. On Windows, drive-letter case
|
|
84
|
+
(``C:`` vs ``c:``) is similarly not normalized.
|
|
85
|
+
"""
|
|
86
|
+
if not uri.startswith("file:"):
|
|
87
|
+
return uri
|
|
88
|
+
fs = to_fs_path(uri)
|
|
89
|
+
if fs is None:
|
|
90
|
+
return uri
|
|
91
|
+
normalized = from_fs_path(fs)
|
|
92
|
+
return normalized if normalized is not None else uri
|
|
93
|
+
|
|
64
94
|
|
|
65
95
|
class JavaFunctionalLspServer(LanguageServer):
|
|
66
96
|
def __init__(self) -> None:
|
|
@@ -75,6 +105,33 @@ class JavaFunctionalLspServer(LanguageServer):
|
|
|
75
105
|
self._skip_jdtls: bool = False
|
|
76
106
|
self._skip_jdtls_registration: bool = False
|
|
77
107
|
self._init_generation: int = 0
|
|
108
|
+
# URIs the client has opened at least once this session (normalized form).
|
|
109
|
+
# Jdtls indexes the whole workspace and publishes diagnostics for files the
|
|
110
|
+
# client never touched; we only republish for URIs present here so unrelated
|
|
111
|
+
# modules don't flood the client. Cumulative (not cleared on didClose) to
|
|
112
|
+
# survive the async race between didClose and late jdtls publishes.
|
|
113
|
+
# Bounded LRU: insertion-ordered, oldest evicted on overflow so a long
|
|
114
|
+
# session navigating many files can't grow the set unboundedly.
|
|
115
|
+
# Reset on initialize.
|
|
116
|
+
self._session_opened_uris: OrderedDict[str, None] = OrderedDict()
|
|
117
|
+
|
|
118
|
+
def _record_opened(self, uri: str) -> None:
|
|
119
|
+
"""Record *uri* as opened this session, evicting the oldest entry at cap.
|
|
120
|
+
|
|
121
|
+
Stores the normalized form so gate lookups in ``_on_jdtls_diagnostics``
|
|
122
|
+
can use a fast raw-first check without redundant normalization.
|
|
123
|
+
"""
|
|
124
|
+
uri = _normalize_uri(uri)
|
|
125
|
+
if uri in self._session_opened_uris:
|
|
126
|
+
self._session_opened_uris.move_to_end(uri)
|
|
127
|
+
return
|
|
128
|
+
self._session_opened_uris[uri] = None
|
|
129
|
+
# Size check uses > so the dict stabilises at exactly _MAX_OPENED_URIS
|
|
130
|
+
# entries after the pop: we insert first (reaching MAX+1 momentarily),
|
|
131
|
+
# then evict. The transient overshoot is invisible to other callers
|
|
132
|
+
# because asyncio is single-threaded and no await intervenes here.
|
|
133
|
+
if len(self._session_opened_uris) > _MAX_OPENED_URIS:
|
|
134
|
+
self._session_opened_uris.popitem(last=False)
|
|
78
135
|
|
|
79
136
|
def _on_jdtls_diagnostics(self, uri: str, diagnostics: list[Any]) -> None:
|
|
80
137
|
"""Called when jdtls publishes diagnostics — merge with custom and re-publish.
|
|
@@ -107,10 +164,25 @@ class JavaFunctionalLspServer(LanguageServer):
|
|
|
107
164
|
# First READY transition only — subsequent diagnostics for the same
|
|
108
165
|
# module are no-ops to avoid spawning O(files) redundant tasks.
|
|
109
166
|
_fire_and_forget(_apply_module_diff(self._proxy, module_uri))
|
|
167
|
+
# Only republish for files the client has opened at any point this
|
|
168
|
+
# session (cumulative). Jdtls scans the whole workspace; without this
|
|
169
|
+
# guard, diagnostics for 200+ unrelated modules leak out as
|
|
170
|
+
# <new-diagnostics> tags (see #71). Non-file URIs (jdt://, untitled://,
|
|
171
|
+
# etc.) are never recorded by _record_opened so they're dropped here.
|
|
172
|
+
#
|
|
173
|
+
# Two-phase lookup: check the raw URI first (O(1), no allocation) since
|
|
174
|
+
# jdtls typically echoes URIs in the same form as the client — the fast
|
|
175
|
+
# path avoids the to_fs_path/from_fs_path round-trip on every publish.
|
|
176
|
+
# Fall back to the normalized form only on a miss to handle encoding
|
|
177
|
+
# mismatches (e.g., client sends literal space, jdtls echoes %20).
|
|
178
|
+
if uri not in self._session_opened_uris and _normalize_uri(uri) not in self._session_opened_uris:
|
|
179
|
+
return
|
|
110
180
|
try:
|
|
111
181
|
_analyze_and_publish(uri)
|
|
112
182
|
except FileNotFoundError:
|
|
113
|
-
|
|
183
|
+
# The file was in the opened-URIs set but vanished from disk
|
|
184
|
+
# between didOpen and this late jdtls publish. Benign.
|
|
185
|
+
pass
|
|
114
186
|
except Exception as e:
|
|
115
187
|
logger.error("Error re-publishing diagnostics for %s: %s", uri, e)
|
|
116
188
|
|
|
@@ -389,6 +461,7 @@ def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|
|
389
461
|
server._skip_jdtls = False
|
|
390
462
|
server._skip_jdtls_registration = False
|
|
391
463
|
server._init_generation += 1
|
|
464
|
+
server._session_opened_uris.clear()
|
|
392
465
|
_jdtls_capabilities_registered = False
|
|
393
466
|
|
|
394
467
|
jdtls_override = os.environ.get("JAVA_FUNCTIONAL_LSP_JDTLS", "").strip().lower()
|
|
@@ -577,6 +650,9 @@ async def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
|
|
577
650
|
didOpen response isn't delayed by jdtls cold-start.
|
|
578
651
|
"""
|
|
579
652
|
uri = params.text_document.uri
|
|
653
|
+
# Must precede any await: same-loop jdtls diagnostic callbacks need to
|
|
654
|
+
# observe this URI before they arrive to avoid the publish getting gated.
|
|
655
|
+
server._record_opened(uri)
|
|
580
656
|
|
|
581
657
|
if server._skip_jdtls:
|
|
582
658
|
# Skip all jdtls forwarding — custom diagnostics only.
|
|
@@ -635,6 +711,24 @@ async def on_did_close(params: lsp.DidCloseTextDocumentParams) -> None:
|
|
|
635
711
|
_forward_or_queue("textDocument/didClose", _serialize_params(params))
|
|
636
712
|
|
|
637
713
|
|
|
714
|
+
_DIDCHANGE_CONFIG_COOLDOWN = 30.0 # seconds — rate-limit didChangeConfiguration forwarding
|
|
715
|
+
_last_config_forward: float = 0.0
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
@server.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
|
|
719
|
+
def on_did_change_configuration(params: lsp.DidChangeConfigurationParams) -> None:
|
|
720
|
+
"""Forward IDE configuration changes to jdtls (rate-limited)."""
|
|
721
|
+
global _last_config_forward
|
|
722
|
+
if not server._proxy.is_available:
|
|
723
|
+
return
|
|
724
|
+
now = time.monotonic()
|
|
725
|
+
if now - _last_config_forward < _DIDCHANGE_CONFIG_COOLDOWN:
|
|
726
|
+
logger.debug("jdtls: throttling didChangeConfiguration (%.0fs cooldown)", _DIDCHANGE_CONFIG_COOLDOWN)
|
|
727
|
+
return
|
|
728
|
+
_last_config_forward = now
|
|
729
|
+
_fire_and_forget(server._proxy.send_notification("workspace/didChangeConfiguration", _serialize_params(params)))
|
|
730
|
+
|
|
731
|
+
|
|
638
732
|
async def _lazy_start_jdtls(file_uri: str) -> None:
|
|
639
733
|
"""Background task: start jdtls scoped to the module containing *file_uri*.
|
|
640
734
|
|
|
@@ -173,6 +173,27 @@ def _ensure_workspace() -> None:
|
|
|
173
173
|
class TestServerInternals:
|
|
174
174
|
"""Direct-call tests for server.py helpers — provides in-process coverage."""
|
|
175
175
|
|
|
176
|
+
@pytest.fixture(autouse=True)
|
|
177
|
+
def _reset_server_state(self) -> Any:
|
|
178
|
+
"""Reset mutable singleton state on ``server`` before and after every
|
|
179
|
+
test. Guarantees (a) a test that crashes between .add and .discard
|
|
180
|
+
can't leak into its successor, and (b) residue from another test
|
|
181
|
+
module (notably ``test_merkle_proxy``) can't leak into this class."""
|
|
182
|
+
import java_functional_lsp.server as srv_mod
|
|
183
|
+
from java_functional_lsp.server import server
|
|
184
|
+
|
|
185
|
+
def _reset() -> None:
|
|
186
|
+
server._session_opened_uris.clear()
|
|
187
|
+
server._proxy.modules._states.clear()
|
|
188
|
+
server._skip_jdtls = False
|
|
189
|
+
server._skip_jdtls_registration = False
|
|
190
|
+
server._init_generation = 0
|
|
191
|
+
srv_mod._jdtls_capabilities_registered = False
|
|
192
|
+
|
|
193
|
+
_reset()
|
|
194
|
+
yield
|
|
195
|
+
_reset()
|
|
196
|
+
|
|
176
197
|
def test_load_config_no_workspace(self) -> None:
|
|
177
198
|
from java_functional_lsp.server import _load_config
|
|
178
199
|
|
|
@@ -281,6 +302,7 @@ class TestServerInternals:
|
|
|
281
302
|
uri=uri, language_id="java", version=1, text="class T { String f() { return null; } }"
|
|
282
303
|
),
|
|
283
304
|
)
|
|
305
|
+
server._record_opened(uri)
|
|
284
306
|
try:
|
|
285
307
|
with patch.object(server, "text_document_publish_diagnostics") as mock_pub:
|
|
286
308
|
server._on_jdtls_diagnostics(uri, [])
|
|
@@ -289,6 +311,7 @@ class TestServerInternals:
|
|
|
289
311
|
assert "null-return" in codes
|
|
290
312
|
finally:
|
|
291
313
|
server.workspace.remove_text_document(uri)
|
|
314
|
+
server._session_opened_uris.pop(uri, None)
|
|
292
315
|
|
|
293
316
|
def test_on_jdtls_diagnostics_non_project_skips_ready(self) -> None:
|
|
294
317
|
"""A publishDiagnostics batch with code-16 must NOT mark the module READY."""
|
|
@@ -322,6 +345,38 @@ class TestServerInternals:
|
|
|
322
345
|
|
|
323
346
|
server._proxy.modules.clear()
|
|
324
347
|
uri = "file:///mod/Pr.java"
|
|
348
|
+
server._record_opened(uri)
|
|
349
|
+
real_diag = {
|
|
350
|
+
"code": "268435844",
|
|
351
|
+
"message": "Some unused import",
|
|
352
|
+
"source": "Java",
|
|
353
|
+
"severity": 2,
|
|
354
|
+
"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}},
|
|
355
|
+
}
|
|
356
|
+
try:
|
|
357
|
+
with (
|
|
358
|
+
patch("java_functional_lsp.server._resolve_module_uri", return_value="file:///mod"),
|
|
359
|
+
patch("java_functional_lsp.server._analyze_and_publish") as mock_pub,
|
|
360
|
+
patch("java_functional_lsp.server._fire_and_forget"),
|
|
361
|
+
):
|
|
362
|
+
server._on_jdtls_diagnostics(uri, [real_diag])
|
|
363
|
+
mock_pub.assert_called_once_with(uri)
|
|
364
|
+
assert server._proxy.modules.get_state("file:///mod") == ModuleState.READY
|
|
365
|
+
finally:
|
|
366
|
+
server._proxy.modules.clear()
|
|
367
|
+
server._session_opened_uris.pop(uri, None)
|
|
368
|
+
|
|
369
|
+
def test_on_jdtls_diagnostics_skips_publish_for_unopened_uri(self) -> None:
|
|
370
|
+
"""Jdtls diagnostics for files the client never opened must not republish (#71)."""
|
|
371
|
+
from unittest.mock import patch
|
|
372
|
+
|
|
373
|
+
from java_functional_lsp.proxy import ModuleState
|
|
374
|
+
from java_functional_lsp.server import server
|
|
375
|
+
|
|
376
|
+
server._proxy.modules.clear()
|
|
377
|
+
uri = "file:///other/Module/Foo.java"
|
|
378
|
+
# Sanity-check: autouse fixture guarantees a clean set; explicit for readability.
|
|
379
|
+
assert uri not in server._session_opened_uris
|
|
325
380
|
real_diag = {
|
|
326
381
|
"code": "268435844",
|
|
327
382
|
"message": "Some unused import",
|
|
@@ -329,14 +384,375 @@ class TestServerInternals:
|
|
|
329
384
|
"severity": 2,
|
|
330
385
|
"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}},
|
|
331
386
|
}
|
|
387
|
+
try:
|
|
388
|
+
with (
|
|
389
|
+
patch("java_functional_lsp.server._resolve_module_uri", return_value="file:///other/Module"),
|
|
390
|
+
patch("java_functional_lsp.server._analyze_and_publish") as mock_pub,
|
|
391
|
+
patch("java_functional_lsp.server._fire_and_forget"),
|
|
392
|
+
):
|
|
393
|
+
server._on_jdtls_diagnostics(uri, [real_diag])
|
|
394
|
+
mock_pub.assert_not_called()
|
|
395
|
+
# Module-READY bookkeeping must still run even when the publish is gated.
|
|
396
|
+
assert server._proxy.modules.get_state("file:///other/Module") == ModuleState.READY
|
|
397
|
+
finally:
|
|
398
|
+
server._proxy.modules.clear()
|
|
399
|
+
|
|
400
|
+
async def test_on_did_open_records_uri(self) -> None:
|
|
401
|
+
"""on_did_open must add the URI to the session's opened-URIs set."""
|
|
402
|
+
from unittest.mock import patch
|
|
403
|
+
|
|
404
|
+
from java_functional_lsp.server import on_did_open, server
|
|
405
|
+
|
|
406
|
+
_ensure_workspace()
|
|
407
|
+
uri = "file:///test/Opened.java"
|
|
408
|
+
server._session_opened_uris.pop(uri, None)
|
|
409
|
+
old_skip = server._skip_jdtls
|
|
410
|
+
server._skip_jdtls = True # avoid exercising jdtls paths
|
|
411
|
+
try:
|
|
412
|
+
with patch("java_functional_lsp.server._analyze_and_publish"):
|
|
413
|
+
await on_did_open(
|
|
414
|
+
lsp.DidOpenTextDocumentParams(
|
|
415
|
+
text_document=lsp.TextDocumentItem(
|
|
416
|
+
uri=uri, language_id="java", version=1, text="class Opened {}"
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
assert uri in server._session_opened_uris
|
|
421
|
+
finally:
|
|
422
|
+
server._session_opened_uris.pop(uri, None)
|
|
423
|
+
server._skip_jdtls = old_skip
|
|
424
|
+
|
|
425
|
+
async def test_on_did_open_stores_normalized_form(self) -> None:
|
|
426
|
+
"""on_did_open must store the normalized URI, not the raw client form.
|
|
427
|
+
|
|
428
|
+
_record_opened normalizes before storing. If the client sends a URI
|
|
429
|
+
with a literal space and jdtls echoes it with ``%20``, the gate lookup
|
|
430
|
+
must still pass — which only works if the stored key is the normalized
|
|
431
|
+
form.
|
|
432
|
+
"""
|
|
433
|
+
from unittest.mock import patch
|
|
434
|
+
|
|
435
|
+
from java_functional_lsp.server import _normalize_uri, on_did_open, server
|
|
436
|
+
|
|
437
|
+
_ensure_workspace()
|
|
438
|
+
raw_uri = "file:///test/has space.java"
|
|
439
|
+
normalized_uri = _normalize_uri(raw_uri)
|
|
440
|
+
assert normalized_uri != raw_uri, "test setup: raw and normalized must differ"
|
|
441
|
+
for u in (raw_uri, normalized_uri):
|
|
442
|
+
server._session_opened_uris.pop(u, None)
|
|
443
|
+
old_skip = server._skip_jdtls
|
|
444
|
+
server._skip_jdtls = True
|
|
445
|
+
try:
|
|
446
|
+
with patch("java_functional_lsp.server._analyze_and_publish"):
|
|
447
|
+
await on_did_open(
|
|
448
|
+
lsp.DidOpenTextDocumentParams(
|
|
449
|
+
text_document=lsp.TextDocumentItem(
|
|
450
|
+
uri=raw_uri, language_id="java", version=1, text="class S {}"
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
# Raw form must NOT be stored; normalized form MUST be.
|
|
455
|
+
assert raw_uri not in server._session_opened_uris
|
|
456
|
+
assert normalized_uri in server._session_opened_uris
|
|
457
|
+
finally:
|
|
458
|
+
for u in (raw_uri, normalized_uri):
|
|
459
|
+
server._session_opened_uris.pop(u, None)
|
|
460
|
+
server._skip_jdtls = old_skip
|
|
461
|
+
|
|
462
|
+
def test_on_initialize_clears_opened_uris(self) -> None:
|
|
463
|
+
"""Re-initialize must reset the session's opened-URIs set."""
|
|
464
|
+
from java_functional_lsp.server import on_initialize, server
|
|
465
|
+
|
|
466
|
+
server._record_opened("file:///stale/Leftover.java")
|
|
467
|
+
on_initialize(
|
|
468
|
+
lsp.InitializeParams(
|
|
469
|
+
process_id=1,
|
|
470
|
+
root_uri="file:///tmp",
|
|
471
|
+
capabilities=lsp.ClientCapabilities(),
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
assert not server._session_opened_uris
|
|
475
|
+
|
|
476
|
+
def test_opened_uris_evicts_oldest_at_cap(self) -> None:
|
|
477
|
+
"""_record_opened enforces a FIFO cap so the set can't grow unbounded."""
|
|
478
|
+
from unittest.mock import patch
|
|
479
|
+
|
|
480
|
+
from java_functional_lsp.server import server
|
|
481
|
+
|
|
482
|
+
server._session_opened_uris.clear()
|
|
483
|
+
try:
|
|
484
|
+
with patch("java_functional_lsp.server._MAX_OPENED_URIS", 3):
|
|
485
|
+
for i in range(3):
|
|
486
|
+
server._record_opened(f"file:///u/F{i}.java")
|
|
487
|
+
assert list(server._session_opened_uris) == [
|
|
488
|
+
"file:///u/F0.java",
|
|
489
|
+
"file:///u/F1.java",
|
|
490
|
+
"file:///u/F2.java",
|
|
491
|
+
]
|
|
492
|
+
# Fourth insert must evict the oldest.
|
|
493
|
+
server._record_opened("file:///u/F3.java")
|
|
494
|
+
assert list(server._session_opened_uris) == [
|
|
495
|
+
"file:///u/F1.java",
|
|
496
|
+
"file:///u/F2.java",
|
|
497
|
+
"file:///u/F3.java",
|
|
498
|
+
]
|
|
499
|
+
# Re-opening an existing URI moves it to the end, no eviction.
|
|
500
|
+
server._record_opened("file:///u/F1.java")
|
|
501
|
+
assert list(server._session_opened_uris) == [
|
|
502
|
+
"file:///u/F2.java",
|
|
503
|
+
"file:///u/F3.java",
|
|
504
|
+
"file:///u/F1.java",
|
|
505
|
+
]
|
|
506
|
+
finally:
|
|
507
|
+
server._session_opened_uris.clear()
|
|
508
|
+
|
|
509
|
+
def test_opened_uris_cap_one_always_evicts_previous(self) -> None:
|
|
510
|
+
"""With cap=1, every new URI evicts the previous one."""
|
|
511
|
+
from unittest.mock import patch
|
|
512
|
+
|
|
513
|
+
from java_functional_lsp.server import server
|
|
514
|
+
|
|
515
|
+
server._session_opened_uris.clear()
|
|
516
|
+
try:
|
|
517
|
+
with patch("java_functional_lsp.server._MAX_OPENED_URIS", 1):
|
|
518
|
+
server._record_opened("file:///u/A.java")
|
|
519
|
+
assert list(server._session_opened_uris) == ["file:///u/A.java"]
|
|
520
|
+
server._record_opened("file:///u/B.java")
|
|
521
|
+
assert list(server._session_opened_uris) == ["file:///u/B.java"]
|
|
522
|
+
# Re-opening the sole entry is a no-op (move_to_end, no eviction).
|
|
523
|
+
server._record_opened("file:///u/B.java")
|
|
524
|
+
assert list(server._session_opened_uris) == ["file:///u/B.java"]
|
|
525
|
+
finally:
|
|
526
|
+
server._session_opened_uris.clear()
|
|
527
|
+
|
|
528
|
+
def test_opened_uris_burst_never_exceeds_cap(self) -> None:
|
|
529
|
+
"""Inserting N >> cap items must leave the dict at exactly cap entries."""
|
|
530
|
+
from unittest.mock import patch
|
|
531
|
+
|
|
532
|
+
from java_functional_lsp.server import server
|
|
533
|
+
|
|
534
|
+
server._session_opened_uris.clear()
|
|
535
|
+
cap = 5
|
|
536
|
+
burst = 50
|
|
537
|
+
try:
|
|
538
|
+
with patch("java_functional_lsp.server._MAX_OPENED_URIS", cap):
|
|
539
|
+
for i in range(burst):
|
|
540
|
+
server._record_opened(f"file:///u/F{i}.java")
|
|
541
|
+
assert len(server._session_opened_uris) == cap
|
|
542
|
+
# Must contain the last `cap` inserted URIs.
|
|
543
|
+
assert list(server._session_opened_uris) == [f"file:///u/F{i}.java" for i in range(burst - cap, burst)]
|
|
544
|
+
finally:
|
|
545
|
+
server._session_opened_uris.clear()
|
|
546
|
+
|
|
547
|
+
async def test_on_did_close_preserves_opened_uris(self) -> None:
|
|
548
|
+
"""Cumulative design: didClose must NOT evict — late jdtls publishes still go through."""
|
|
549
|
+
from unittest.mock import patch
|
|
550
|
+
|
|
551
|
+
from java_functional_lsp.server import on_did_close, server
|
|
552
|
+
|
|
553
|
+
_ensure_workspace()
|
|
554
|
+
uri = "file:///test/Closed.java"
|
|
555
|
+
server._record_opened(uri)
|
|
556
|
+
old_skip = server._skip_jdtls
|
|
557
|
+
server._skip_jdtls = True
|
|
558
|
+
try:
|
|
559
|
+
with patch.object(server, "text_document_publish_diagnostics"):
|
|
560
|
+
await on_did_close(
|
|
561
|
+
lsp.DidCloseTextDocumentParams(
|
|
562
|
+
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
assert uri in server._session_opened_uris
|
|
566
|
+
finally:
|
|
567
|
+
server._session_opened_uris.pop(uri, None)
|
|
568
|
+
server._skip_jdtls = old_skip
|
|
569
|
+
|
|
570
|
+
async def test_on_did_close_preserves_opened_uris_jdtls_available(self) -> None:
|
|
571
|
+
"""Cumulative design holds when jdtls IS available (not just the skip path)."""
|
|
572
|
+
from unittest.mock import AsyncMock, PropertyMock, patch
|
|
573
|
+
|
|
574
|
+
from java_functional_lsp.server import on_did_close, server
|
|
575
|
+
|
|
576
|
+
_ensure_workspace()
|
|
577
|
+
uri = "file:///test/ClosedLive.java"
|
|
578
|
+
server._record_opened(uri)
|
|
579
|
+
old_skip = server._skip_jdtls
|
|
580
|
+
server._skip_jdtls = False
|
|
581
|
+
try:
|
|
582
|
+
with (
|
|
583
|
+
patch.object(server, "text_document_publish_diagnostics"),
|
|
584
|
+
patch.object(type(server._proxy), "is_available", new_callable=PropertyMock, return_value=True),
|
|
585
|
+
patch.object(server._proxy, "send_notification", new=AsyncMock()),
|
|
586
|
+
):
|
|
587
|
+
await on_did_close(
|
|
588
|
+
lsp.DidCloseTextDocumentParams(
|
|
589
|
+
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
assert uri in server._session_opened_uris
|
|
593
|
+
finally:
|
|
594
|
+
server._session_opened_uris.pop(uri, None)
|
|
595
|
+
server._skip_jdtls = old_skip
|
|
596
|
+
|
|
597
|
+
async def test_on_did_open_records_uri_before_first_await(self) -> None:
|
|
598
|
+
"""Records the URI before yielding to the event loop.
|
|
599
|
+
|
|
600
|
+
Jdtls dispatches diagnostics on the same loop as ``on_did_open``. If
|
|
601
|
+
the record happened after the first ``await``, a diagnostic batch that
|
|
602
|
+
lands between the ``await`` and the record would be gated out. The
|
|
603
|
+
pre-await record is the only thing that guarantees same-loop
|
|
604
|
+
correctness — regression-guard it here.
|
|
605
|
+
"""
|
|
606
|
+
from unittest.mock import AsyncMock, PropertyMock, patch
|
|
607
|
+
|
|
608
|
+
from java_functional_lsp.server import on_did_open, server
|
|
609
|
+
|
|
610
|
+
_ensure_workspace()
|
|
611
|
+
uri = "file:///test/PreAwait.java"
|
|
612
|
+
server._session_opened_uris.pop(uri, None)
|
|
613
|
+
|
|
614
|
+
observed_before_await: dict[str, bool] = {}
|
|
615
|
+
|
|
616
|
+
async def _spy_send(*_args: Any, **_kwargs: Any) -> None:
|
|
617
|
+
# This spy is injected as server._proxy.send_notification, which is
|
|
618
|
+
# the first ``await`` reached in the is_available=True branch of
|
|
619
|
+
# on_did_open (line: `await server._proxy.send_notification(...)`).
|
|
620
|
+
# If the URI was recorded before that point it must already be in
|
|
621
|
+
# the set. If a future refactor moves an ``await`` earlier in
|
|
622
|
+
# on_did_open, this spy must be updated to hook that new first
|
|
623
|
+
# await so the ordering guarantee is still tested.
|
|
624
|
+
observed_before_await["present"] = uri in server._session_opened_uris
|
|
625
|
+
|
|
626
|
+
old_skip = server._skip_jdtls
|
|
627
|
+
server._skip_jdtls = False
|
|
628
|
+
try:
|
|
629
|
+
with (
|
|
630
|
+
patch("java_functional_lsp.server._analyze_and_publish"),
|
|
631
|
+
patch.object(type(server._proxy), "is_available", new_callable=PropertyMock, return_value=True),
|
|
632
|
+
patch.object(server._proxy, "send_notification", new=AsyncMock(side_effect=_spy_send)),
|
|
633
|
+
patch.object(server._proxy, "add_module_if_new", new=AsyncMock()),
|
|
634
|
+
):
|
|
635
|
+
await on_did_open(
|
|
636
|
+
lsp.DidOpenTextDocumentParams(
|
|
637
|
+
text_document=lsp.TextDocumentItem(uri=uri, language_id="java", version=1, text="class P {}")
|
|
638
|
+
)
|
|
639
|
+
)
|
|
640
|
+
assert observed_before_await.get("present") is True, (
|
|
641
|
+
"URI must be recorded before on_did_open yields to the event loop"
|
|
642
|
+
)
|
|
643
|
+
finally:
|
|
644
|
+
server._session_opened_uris.pop(uri, None)
|
|
645
|
+
server._skip_jdtls = old_skip
|
|
646
|
+
|
|
647
|
+
async def test_on_did_open_records_uri_before_first_await_queue_mode(self) -> None:
|
|
648
|
+
"""URI is recorded before any background task starts in queue (lazy-start) mode.
|
|
649
|
+
|
|
650
|
+
In the queue mode path (jdtls on PATH but not yet started), there is no
|
|
651
|
+
early ``await`` before ``_analyze_and_publish`` — the record must still
|
|
652
|
+
happen synchronously before the coroutine first yields. This test
|
|
653
|
+
verifies that the ``_record_opened`` call precedes any async work by
|
|
654
|
+
checking the set state directly after the call, with jdtls startup mocked
|
|
655
|
+
out so no background coroutine fires.
|
|
656
|
+
"""
|
|
657
|
+
from unittest.mock import AsyncMock, PropertyMock, patch
|
|
658
|
+
|
|
659
|
+
from java_functional_lsp.server import on_did_open, server
|
|
660
|
+
|
|
661
|
+
_ensure_workspace()
|
|
662
|
+
uri = "file:///test/QueueMode.java"
|
|
663
|
+
server._session_opened_uris.pop(uri, None)
|
|
664
|
+
old_skip = server._skip_jdtls
|
|
665
|
+
server._skip_jdtls = False
|
|
666
|
+
try:
|
|
667
|
+
with (
|
|
668
|
+
patch("java_functional_lsp.server._analyze_and_publish"),
|
|
669
|
+
# Simulate jdtls not yet available but on PATH and not failed.
|
|
670
|
+
patch.object(type(server._proxy), "is_available", new_callable=PropertyMock, return_value=False),
|
|
671
|
+
patch.object(server._proxy, "_jdtls_on_path", True),
|
|
672
|
+
patch.object(server._proxy, "_start_failed", False),
|
|
673
|
+
patch.object(server._proxy, "_lazy_start_fired", False),
|
|
674
|
+
patch.object(server._proxy, "queue_notification"),
|
|
675
|
+
# Prevent the real lazy-start background task from firing.
|
|
676
|
+
patch("java_functional_lsp.server._lazy_start_jdtls", new=AsyncMock()),
|
|
677
|
+
patch("java_functional_lsp.server._fire_and_forget"),
|
|
678
|
+
):
|
|
679
|
+
await on_did_open(
|
|
680
|
+
lsp.DidOpenTextDocumentParams(
|
|
681
|
+
text_document=lsp.TextDocumentItem(uri=uri, language_id="java", version=1, text="class Q {}")
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
# URI must be present regardless of which branch on_did_open took.
|
|
685
|
+
assert uri in server._session_opened_uris
|
|
686
|
+
finally:
|
|
687
|
+
server._session_opened_uris.pop(uri, None)
|
|
688
|
+
server._skip_jdtls = old_skip
|
|
689
|
+
|
|
690
|
+
def test_on_jdtls_diagnostics_non_project_unopened_skips_both(self) -> None:
|
|
691
|
+
"""A code-16 batch for an unopened URI skips READY *and* publish.
|
|
692
|
+
|
|
693
|
+
Covers the interaction of two independent gates: the code-16 guard on
|
|
694
|
+
module-READY bookkeeping, and the opened-URIs gate on republishing.
|
|
695
|
+
Neither should fire.
|
|
696
|
+
"""
|
|
697
|
+
from unittest.mock import patch
|
|
698
|
+
|
|
699
|
+
from java_functional_lsp.proxy import ModuleState
|
|
700
|
+
from java_functional_lsp.server import server
|
|
701
|
+
|
|
702
|
+
server._proxy.modules.clear()
|
|
703
|
+
uri = "file:///other/Module/Stranger.java"
|
|
704
|
+
# Sanity-check: autouse fixture guarantees a clean set; explicit for readability.
|
|
705
|
+
assert uri not in server._session_opened_uris
|
|
706
|
+
non_project_diag = {
|
|
707
|
+
"code": "16",
|
|
708
|
+
"message": "Stranger.java is a non-project file, only syntax errors are reported",
|
|
709
|
+
"source": "Java",
|
|
710
|
+
"severity": 2,
|
|
711
|
+
"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}},
|
|
712
|
+
}
|
|
332
713
|
with (
|
|
333
|
-
patch("java_functional_lsp.server._resolve_module_uri", return_value="file:///
|
|
334
|
-
patch("java_functional_lsp.server._analyze_and_publish"),
|
|
714
|
+
patch("java_functional_lsp.server._resolve_module_uri", return_value="file:///other/Module"),
|
|
715
|
+
patch("java_functional_lsp.server._analyze_and_publish") as mock_pub,
|
|
335
716
|
patch("java_functional_lsp.server._fire_and_forget"),
|
|
336
717
|
):
|
|
337
|
-
server._on_jdtls_diagnostics(uri, [
|
|
338
|
-
|
|
718
|
+
server._on_jdtls_diagnostics(uri, [non_project_diag])
|
|
719
|
+
mock_pub.assert_not_called()
|
|
720
|
+
assert server._proxy.modules.get_state("file:///other/Module") != ModuleState.READY
|
|
721
|
+
|
|
722
|
+
def test_normalize_uri_passes_through_file_uri(self) -> None:
|
|
723
|
+
from java_functional_lsp.server import _normalize_uri
|
|
724
|
+
|
|
725
|
+
assert _normalize_uri("file:///a/B.java") == "file:///a/B.java"
|
|
726
|
+
|
|
727
|
+
def test_normalize_uri_unifies_space_encoding(self) -> None:
|
|
728
|
+
"""A literal space and %20 must canonicalize to the same URI."""
|
|
729
|
+
from java_functional_lsp.server import _normalize_uri
|
|
730
|
+
|
|
731
|
+
assert _normalize_uri("file:///a/has space.java") == _normalize_uri("file:///a/has%20space.java")
|
|
732
|
+
|
|
733
|
+
def test_normalize_uri_passes_through_non_file_scheme(self) -> None:
|
|
734
|
+
"""Non-file URIs (jdt://, untitled://) are returned unchanged."""
|
|
735
|
+
from java_functional_lsp.server import _normalize_uri
|
|
736
|
+
|
|
737
|
+
assert _normalize_uri("jdt://contents/foo.Bar") == "jdt://contents/foo.Bar"
|
|
738
|
+
assert _normalize_uri("untitled://Untitled-1") == "untitled://Untitled-1"
|
|
739
|
+
|
|
740
|
+
def test_on_jdtls_diagnostics_matches_opened_uri_across_encodings(self) -> None:
|
|
741
|
+
"""Gate compares URIs via _normalize_uri so %20 vs literal space still matches."""
|
|
742
|
+
from unittest.mock import patch
|
|
743
|
+
|
|
744
|
+
from java_functional_lsp.server import server
|
|
745
|
+
|
|
339
746
|
server._proxy.modules.clear()
|
|
747
|
+
# Client records with literal space; jdtls echoes with %20. These must match.
|
|
748
|
+
server._record_opened("file:///a/has space.java")
|
|
749
|
+
with (
|
|
750
|
+
patch("java_functional_lsp.server._resolve_module_uri", return_value=None),
|
|
751
|
+
patch("java_functional_lsp.server._analyze_and_publish") as mock_pub,
|
|
752
|
+
patch("java_functional_lsp.server._fire_and_forget"),
|
|
753
|
+
):
|
|
754
|
+
server._on_jdtls_diagnostics("file:///a/has%20space.java", [])
|
|
755
|
+
mock_pub.assert_called_once_with("file:///a/has%20space.java")
|
|
340
756
|
|
|
341
757
|
def test_init_capabilities_exclude_jdtls_features(self) -> None:
|
|
342
758
|
"""Static capabilities must NOT include hover/definition/references/completion/documentSymbol.
|
|
@@ -1547,6 +1963,50 @@ class TestFindLombokJar:
|
|
|
1547
1963
|
assert any("does not exist" in r.getMessage() for r in caplog.records)
|
|
1548
1964
|
|
|
1549
1965
|
|
|
1966
|
+
class TestDidChangeConfiguration:
|
|
1967
|
+
"""Tests for workspace/didChangeConfiguration forwarding."""
|
|
1968
|
+
|
|
1969
|
+
def test_forwards_to_jdtls_when_available(self) -> None:
|
|
1970
|
+
from unittest.mock import AsyncMock, patch
|
|
1971
|
+
|
|
1972
|
+
import java_functional_lsp.server as srv_mod
|
|
1973
|
+
from java_functional_lsp.server import on_did_change_configuration, server
|
|
1974
|
+
|
|
1975
|
+
srv_mod._last_config_forward = 0.0 # reset cooldown
|
|
1976
|
+
server._proxy._available = True
|
|
1977
|
+
server._proxy.send_notification = AsyncMock() # type: ignore[assignment]
|
|
1978
|
+
params = lsp.DidChangeConfigurationParams(settings={"java": {"autobuild": {"enabled": True}}})
|
|
1979
|
+
with patch("java_functional_lsp.server._fire_and_forget") as mock_faf:
|
|
1980
|
+
on_did_change_configuration(params)
|
|
1981
|
+
mock_faf.assert_called_once()
|
|
1982
|
+
|
|
1983
|
+
def test_throttled_within_cooldown(self) -> None:
|
|
1984
|
+
from unittest.mock import patch
|
|
1985
|
+
|
|
1986
|
+
import java_functional_lsp.server as srv_mod
|
|
1987
|
+
from java_functional_lsp.server import on_did_change_configuration, server
|
|
1988
|
+
|
|
1989
|
+
server._proxy._available = True
|
|
1990
|
+
import time
|
|
1991
|
+
|
|
1992
|
+
srv_mod._last_config_forward = time.monotonic() # just sent
|
|
1993
|
+
params = lsp.DidChangeConfigurationParams(settings={})
|
|
1994
|
+
with patch("java_functional_lsp.server._fire_and_forget") as mock_faf:
|
|
1995
|
+
on_did_change_configuration(params)
|
|
1996
|
+
mock_faf.assert_not_called()
|
|
1997
|
+
|
|
1998
|
+
def test_skipped_when_proxy_unavailable(self) -> None:
|
|
1999
|
+
from unittest.mock import patch
|
|
2000
|
+
|
|
2001
|
+
from java_functional_lsp.server import on_did_change_configuration, server
|
|
2002
|
+
|
|
2003
|
+
server._proxy._available = False
|
|
2004
|
+
params = lsp.DidChangeConfigurationParams(settings={})
|
|
2005
|
+
with patch("java_functional_lsp.server._fire_and_forget") as mock_faf:
|
|
2006
|
+
on_did_change_configuration(params)
|
|
2007
|
+
mock_faf.assert_not_called()
|
|
2008
|
+
|
|
2009
|
+
|
|
1550
2010
|
class TestCacheClear:
|
|
1551
2011
|
"""Tests for _clear_cache_on_version_change() and _compute_cache_marker()."""
|
|
1552
2012
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/ISSUE_TEMPLATE/bug-report.md
RENAMED
|
File without changes
|
{java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/ISSUE_TEMPLATE/feature-request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/release-drafter.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/.github/workflows/update-homebrew.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/editors/intellij/lsp4ij-template.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.11 → java_functional_lsp-0.9.13}/src/java_functional_lsp/analyzers/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|