java-functional-lsp 0.7.4__tar.gz → 0.7.5__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.7.4 → java_functional_lsp-0.7.5}/PKG-INFO +1 -1
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/pyproject.toml +1 -1
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/proxy.py +109 -19
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/server.py +53 -17
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_proxy.py +37 -5
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_server.py +72 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.gitignore +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/LICENSE +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/README.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/SKILL.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/analyzers/base.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/src/java_functional_lsp/fixes.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/__init__.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/conftest.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_base.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_config.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_e2e_jdtls.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_functional_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_mutation_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/tests/test_suppress.py +0 -0
- {java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-functional-lsp
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "java-functional-lsp"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.5"
|
|
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,7 @@ import platform
|
|
|
12
12
|
import re
|
|
13
13
|
import shutil
|
|
14
14
|
import subprocess
|
|
15
|
+
from collections import deque
|
|
15
16
|
from collections.abc import Callable, Mapping
|
|
16
17
|
from functools import lru_cache
|
|
17
18
|
from pathlib import Path
|
|
@@ -19,7 +20,8 @@ from typing import Any
|
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
|
-
REQUEST_TIMEOUT = 30.0 # seconds
|
|
23
|
+
REQUEST_TIMEOUT = 30.0 # seconds — per-request timeout for normal operations
|
|
24
|
+
_INITIALIZE_TIMEOUT = 120.0 # seconds — module-scoped init can still be slow (Maven classpath resolution)
|
|
23
25
|
DEFAULT_JVM_MAX_HEAP = "4g"
|
|
24
26
|
_STDERR_LINE_MAX = 1000
|
|
25
27
|
|
|
@@ -316,6 +318,78 @@ async def read_message(reader: asyncio.StreamReader) -> dict[str, Any] | None:
|
|
|
316
318
|
_BUILD_FILES = ("pom.xml", "build.gradle", "build.gradle.kts")
|
|
317
319
|
_WORKSPACE_DID_CHANGE_FOLDERS = "workspace/didChangeWorkspaceFolders"
|
|
318
320
|
_MAX_QUEUED_NOTIFICATIONS = 200
|
|
321
|
+
_MODULE_READY_TIMEOUT = 30.0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class ModuleState:
|
|
325
|
+
"""Module import states — UNKNOWN → ADDED → READY."""
|
|
326
|
+
|
|
327
|
+
UNKNOWN = "unknown"
|
|
328
|
+
ADDED = "added"
|
|
329
|
+
READY = "ready"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class ModuleRegistry:
|
|
333
|
+
"""Thread-safe (asyncio) registry tracking jdtls module import states.
|
|
334
|
+
|
|
335
|
+
Uses a plain dict for O(1) hot-path lookups and per-module ``asyncio.Event``
|
|
336
|
+
for adaptive waiting — coroutines blocked on ``wait_until_ready()`` wake
|
|
337
|
+
instantly when ``mark_ready()`` is called, instead of a fixed sleep.
|
|
338
|
+
|
|
339
|
+
Safe without locks because asyncio is single-threaded: dict mutations that
|
|
340
|
+
don't span an ``await`` are atomic.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
def __init__(self) -> None:
|
|
344
|
+
self._states: dict[str, str] = {}
|
|
345
|
+
self._ready_events: dict[str, asyncio.Event] = {}
|
|
346
|
+
|
|
347
|
+
def get_state(self, uri: str) -> str:
|
|
348
|
+
"""O(1) state lookup. Returns ModuleState constant."""
|
|
349
|
+
return self._states.get(uri, ModuleState.UNKNOWN)
|
|
350
|
+
|
|
351
|
+
def is_ready(self, uri: str) -> bool:
|
|
352
|
+
"""O(1) hot-path check — zero overhead when module is ready."""
|
|
353
|
+
return self._states.get(uri) == ModuleState.READY
|
|
354
|
+
|
|
355
|
+
def was_added(self, uri: str) -> bool:
|
|
356
|
+
"""True if module was sent to jdtls (ADDED or READY)."""
|
|
357
|
+
return uri in self._states
|
|
358
|
+
|
|
359
|
+
def mark_added(self, uri: str) -> None:
|
|
360
|
+
"""Mark module as sent to jdtls. Pre-creates the Event for waiters.
|
|
361
|
+
|
|
362
|
+
Must be called before any ``await`` to prevent duplicate add_module calls.
|
|
363
|
+
"""
|
|
364
|
+
self._states[uri] = ModuleState.ADDED
|
|
365
|
+
self._ready_events.setdefault(uri, asyncio.Event())
|
|
366
|
+
|
|
367
|
+
def mark_ready(self, uri: str) -> None:
|
|
368
|
+
"""Mark module as confirmed working. Wakes all coroutines waiting on it."""
|
|
369
|
+
self._states[uri] = ModuleState.READY
|
|
370
|
+
event = self._ready_events.pop(uri, None)
|
|
371
|
+
if event is not None:
|
|
372
|
+
event.set()
|
|
373
|
+
|
|
374
|
+
def clear(self) -> None:
|
|
375
|
+
"""Reset all state. Used by tests."""
|
|
376
|
+
self._states.clear()
|
|
377
|
+
self._ready_events.clear()
|
|
378
|
+
|
|
379
|
+
async def wait_until_ready(self, uri: str, timeout: float = _MODULE_READY_TIMEOUT) -> bool:
|
|
380
|
+
"""Suspend until the module is ready or timeout expires.
|
|
381
|
+
|
|
382
|
+
Returns True if ready, False on timeout. If already READY, returns
|
|
383
|
+
immediately without suspending.
|
|
384
|
+
"""
|
|
385
|
+
event = self._ready_events.setdefault(uri, asyncio.Event())
|
|
386
|
+
if event.is_set():
|
|
387
|
+
return True
|
|
388
|
+
try:
|
|
389
|
+
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
390
|
+
return True
|
|
391
|
+
except asyncio.TimeoutError:
|
|
392
|
+
return False
|
|
319
393
|
|
|
320
394
|
|
|
321
395
|
@lru_cache(maxsize=256)
|
|
@@ -336,6 +410,9 @@ def find_module_root(file_path: str) -> str | None:
|
|
|
336
410
|
|
|
337
411
|
Returns the directory path, or ``None`` if no build file is found before
|
|
338
412
|
reaching the filesystem root. Results are cached by parent directory.
|
|
413
|
+
|
|
414
|
+
**Note:** cache entries are never invalidated. Build files added after the
|
|
415
|
+
first lookup for a given directory will not be detected until process restart.
|
|
339
416
|
"""
|
|
340
417
|
return _cached_module_root(str(Path(file_path).parent))
|
|
341
418
|
|
|
@@ -373,10 +450,10 @@ class JdtlsProxy:
|
|
|
373
450
|
self._start_failed = False
|
|
374
451
|
self._jdtls_on_path = False
|
|
375
452
|
self._lazy_start_fired = False
|
|
376
|
-
self._queued_notifications:
|
|
453
|
+
self._queued_notifications: deque[tuple[str, Any]] = deque(maxlen=_MAX_QUEUED_NOTIFICATIONS)
|
|
377
454
|
self._original_root_uri: str | None = None
|
|
378
455
|
self._initial_module_uri: str | None = None
|
|
379
|
-
self.
|
|
456
|
+
self.modules = ModuleRegistry()
|
|
380
457
|
self._workspace_expanded = False
|
|
381
458
|
|
|
382
459
|
@property
|
|
@@ -437,9 +514,9 @@ class JdtlsProxy:
|
|
|
437
514
|
ws = caps.setdefault("workspace", {})
|
|
438
515
|
ws["workspaceFolders"] = True
|
|
439
516
|
|
|
440
|
-
# Track the initial module as already loaded.
|
|
517
|
+
# Track the initial module as already loaded (mark ADDED before await).
|
|
441
518
|
self._initial_module_uri = module_root_uri
|
|
442
|
-
self.
|
|
519
|
+
self.modules.mark_added(effective_root_uri)
|
|
443
520
|
|
|
444
521
|
# Build a clean environment for jdtls.
|
|
445
522
|
loop = asyncio.get_running_loop()
|
|
@@ -468,7 +545,7 @@ class JdtlsProxy:
|
|
|
468
545
|
if self._process.stderr is not None:
|
|
469
546
|
self._stderr_task = asyncio.create_task(self._stderr_reader(self._process.stderr))
|
|
470
547
|
|
|
471
|
-
result = await self.send_request("initialize", effective_params)
|
|
548
|
+
result = await self.send_request("initialize", effective_params, timeout=_INITIALIZE_TIMEOUT)
|
|
472
549
|
if result is None:
|
|
473
550
|
logger.error("jdtls initialize request failed or timed out")
|
|
474
551
|
await self.stop()
|
|
@@ -508,33 +585,45 @@ class JdtlsProxy:
|
|
|
508
585
|
self._start_failed = True
|
|
509
586
|
self._queued_notifications.clear()
|
|
510
587
|
return started
|
|
588
|
+
except Exception:
|
|
589
|
+
self._start_failed = True
|
|
590
|
+
self._queued_notifications.clear()
|
|
591
|
+
raise
|
|
511
592
|
finally:
|
|
512
593
|
self._starting = False
|
|
513
594
|
|
|
514
595
|
def queue_notification(self, method: str, params: Any) -> None:
|
|
515
596
|
"""Buffer a notification for replay after jdtls starts.
|
|
516
597
|
|
|
517
|
-
|
|
518
|
-
|
|
598
|
+
Uses a ``deque(maxlen=200)`` so oldest entries are dropped in O(1)
|
|
599
|
+
when the queue overflows during long jdtls startup.
|
|
519
600
|
"""
|
|
520
|
-
if len(self._queued_notifications) >= _MAX_QUEUED_NOTIFICATIONS:
|
|
521
|
-
self._queued_notifications.pop(0)
|
|
522
601
|
self._queued_notifications.append((method, params))
|
|
523
602
|
|
|
524
603
|
async def flush_queued_notifications(self) -> None:
|
|
525
604
|
"""Send all queued notifications to jdtls."""
|
|
526
|
-
queue
|
|
605
|
+
queue = list(self._queued_notifications)
|
|
606
|
+
self._queued_notifications.clear()
|
|
527
607
|
for method, params in queue:
|
|
528
608
|
await self.send_notification(method, params)
|
|
529
609
|
|
|
530
|
-
async def add_module_if_new(self, file_uri: str) -> None:
|
|
531
|
-
"""Add the module containing *file_uri* to jdtls if not already added.
|
|
610
|
+
async def add_module_if_new(self, file_uri: str) -> str | None:
|
|
611
|
+
"""Add the module containing *file_uri* to jdtls if not already added.
|
|
612
|
+
|
|
613
|
+
Returns the module URI if a new module was added (UNKNOWN → ADDED),
|
|
614
|
+
or ``None`` if already known or unavailable. The returned URI can be
|
|
615
|
+
used with ``modules.wait_until_ready()`` for adaptive waiting.
|
|
616
|
+
|
|
617
|
+
Calls ``modules.mark_added()`` before any ``await`` to prevent
|
|
618
|
+
duplicate add calls from concurrent coroutines.
|
|
619
|
+
"""
|
|
532
620
|
if not self._available:
|
|
533
|
-
return
|
|
621
|
+
return None
|
|
534
622
|
module_uri = _resolve_module_uri(file_uri)
|
|
535
|
-
if module_uri is None or
|
|
536
|
-
return
|
|
537
|
-
|
|
623
|
+
if module_uri is None or self.modules.was_added(module_uri):
|
|
624
|
+
return None
|
|
625
|
+
# Mark ADDED before await — atomic in asyncio, prevents duplicate sends.
|
|
626
|
+
self.modules.mark_added(module_uri)
|
|
538
627
|
from pygls.uris import to_fs_path
|
|
539
628
|
|
|
540
629
|
logger.info("jdtls: adding module %s", _redact_path(to_fs_path(module_uri)))
|
|
@@ -543,6 +632,7 @@ class JdtlsProxy:
|
|
|
543
632
|
_WORKSPACE_DID_CHANGE_FOLDERS,
|
|
544
633
|
{"event": {"added": [{"uri": module_uri, "name": mod_name}], "removed": []}},
|
|
545
634
|
)
|
|
635
|
+
return module_uri
|
|
546
636
|
|
|
547
637
|
async def expand_full_workspace(self) -> None:
|
|
548
638
|
"""Expand jdtls workspace to the full monorepo root (background task).
|
|
@@ -556,10 +646,10 @@ class JdtlsProxy:
|
|
|
556
646
|
|
|
557
647
|
root_path = to_fs_path(self._original_root_uri) or self._original_root_uri
|
|
558
648
|
root_uri = from_fs_path(root_path) or self._original_root_uri
|
|
559
|
-
if
|
|
649
|
+
if self.modules.was_added(root_uri):
|
|
560
650
|
self._workspace_expanded = True
|
|
561
651
|
return
|
|
562
|
-
self.
|
|
652
|
+
self.modules.mark_added(root_uri)
|
|
563
653
|
|
|
564
654
|
# Remove initial module folder to avoid double-indexing.
|
|
565
655
|
removed: list[dict[str, str]] = []
|
|
@@ -26,7 +26,7 @@ from .analyzers.mutation_checker import MutationChecker
|
|
|
26
26
|
from .analyzers.null_checker import NullChecker
|
|
27
27
|
from .analyzers.spring_checker import SpringChecker
|
|
28
28
|
from .fixes import get_fix, get_fix_registry_keys
|
|
29
|
-
from .proxy import JdtlsProxy
|
|
29
|
+
from .proxy import JdtlsProxy, _resolve_module_uri
|
|
30
30
|
|
|
31
31
|
logger = logging.getLogger(__name__)
|
|
32
32
|
|
|
@@ -337,7 +337,7 @@ def _forward_or_queue(method: str, serialized: Any) -> None:
|
|
|
337
337
|
task = asyncio.create_task(server._proxy.send_notification(method, serialized))
|
|
338
338
|
_bg_tasks.add(task)
|
|
339
339
|
task.add_done_callback(_bg_tasks.discard)
|
|
340
|
-
elif server._proxy.
|
|
340
|
+
elif server._proxy._lazy_start_fired and not server._proxy._start_failed:
|
|
341
341
|
server._proxy.queue_notification(method, serialized)
|
|
342
342
|
|
|
343
343
|
|
|
@@ -440,11 +440,55 @@ async def _expand_workspace_background() -> None:
|
|
|
440
440
|
# they only activate after jdtls starts.
|
|
441
441
|
|
|
442
442
|
|
|
443
|
+
async def _ensure_module_and_forward(method: str, params: Any, file_uri: str) -> Any | None:
|
|
444
|
+
"""Forward a request to jdtls, ensuring the file's module is loaded.
|
|
445
|
+
|
|
446
|
+
Uses ``ModuleRegistry`` for adaptive waiting:
|
|
447
|
+
- **READY**: forward immediately (zero overhead on hot path)
|
|
448
|
+
- **UNKNOWN**: add module, wait until ready (adaptive, not fixed sleep)
|
|
449
|
+
- **ADDED**: module sent but not confirmed — wait until ready
|
|
450
|
+
|
|
451
|
+
When a request succeeds, marks the module as READY so subsequent
|
|
452
|
+
requests skip the wait entirely.
|
|
453
|
+
"""
|
|
454
|
+
proxy = server._proxy
|
|
455
|
+
if not proxy.is_available:
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
module_uri = _resolve_module_uri(file_uri)
|
|
459
|
+
|
|
460
|
+
# Hot path: module already confirmed working.
|
|
461
|
+
if module_uri and proxy.modules.is_ready(module_uri):
|
|
462
|
+
return await proxy.send_request(method, _serialize_params(params))
|
|
463
|
+
|
|
464
|
+
# Cold path: add module if unknown, then wait for ready.
|
|
465
|
+
new_module_uri = await proxy.add_module_if_new(file_uri)
|
|
466
|
+
|
|
467
|
+
serialized = _serialize_params(params)
|
|
468
|
+
result = await proxy.send_request(method, serialized)
|
|
469
|
+
|
|
470
|
+
if result is not None:
|
|
471
|
+
# Success — mark module as ready so future requests are instant.
|
|
472
|
+
if module_uri:
|
|
473
|
+
proxy.modules.mark_ready(module_uri)
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
# Null result and module is not yet ready — wait then retry once.
|
|
477
|
+
# Use a short timeout (5s) so single-caller case doesn't block for 30s.
|
|
478
|
+
# If a concurrent request succeeds, Event.set() wakes us early.
|
|
479
|
+
wait_uri = new_module_uri or module_uri
|
|
480
|
+
if wait_uri and not proxy.modules.is_ready(wait_uri):
|
|
481
|
+
await proxy.modules.wait_until_ready(wait_uri, timeout=5.0)
|
|
482
|
+
# Always retry once after waiting — even on timeout the module may be ready.
|
|
483
|
+
result = await proxy.send_request(method, serialized)
|
|
484
|
+
if result is not None and module_uri:
|
|
485
|
+
proxy.modules.mark_ready(module_uri)
|
|
486
|
+
return result
|
|
487
|
+
|
|
488
|
+
|
|
443
489
|
async def _on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | None:
|
|
444
490
|
"""Forward completion request to jdtls."""
|
|
445
|
-
|
|
446
|
-
return None
|
|
447
|
-
result = await server._proxy.send_request("textDocument/completion", _serialize_params(params))
|
|
491
|
+
result = await _ensure_module_and_forward("textDocument/completion", params, params.text_document.uri)
|
|
448
492
|
if result is None:
|
|
449
493
|
return None
|
|
450
494
|
try:
|
|
@@ -455,9 +499,7 @@ async def _on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | N
|
|
|
455
499
|
|
|
456
500
|
async def _on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
|
|
457
501
|
"""Forward hover request to jdtls."""
|
|
458
|
-
|
|
459
|
-
return None
|
|
460
|
-
result = await server._proxy.send_request("textDocument/hover", _serialize_params(params))
|
|
502
|
+
result = await _ensure_module_and_forward("textDocument/hover", params, params.text_document.uri)
|
|
461
503
|
if result is None:
|
|
462
504
|
return None
|
|
463
505
|
try:
|
|
@@ -468,9 +510,7 @@ async def _on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
|
|
|
468
510
|
|
|
469
511
|
async def _on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | None:
|
|
470
512
|
"""Forward go-to-definition request to jdtls."""
|
|
471
|
-
|
|
472
|
-
return None
|
|
473
|
-
result = await server._proxy.send_request("textDocument/definition", _serialize_params(params))
|
|
513
|
+
result = await _ensure_module_and_forward("textDocument/definition", params, params.text_document.uri)
|
|
474
514
|
if result is None:
|
|
475
515
|
return None
|
|
476
516
|
try:
|
|
@@ -483,9 +523,7 @@ async def _on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | N
|
|
|
483
523
|
|
|
484
524
|
async def _on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | None:
|
|
485
525
|
"""Forward find-references request to jdtls."""
|
|
486
|
-
|
|
487
|
-
return None
|
|
488
|
-
result = await server._proxy.send_request("textDocument/references", _serialize_params(params))
|
|
526
|
+
result = await _ensure_module_and_forward("textDocument/references", params, params.text_document.uri)
|
|
489
527
|
if result is None:
|
|
490
528
|
return None
|
|
491
529
|
try:
|
|
@@ -496,9 +534,7 @@ async def _on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | No
|
|
|
496
534
|
|
|
497
535
|
async def _on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.DocumentSymbol] | None:
|
|
498
536
|
"""Forward document symbol request to jdtls."""
|
|
499
|
-
|
|
500
|
-
return None
|
|
501
|
-
result = await server._proxy.send_request("textDocument/documentSymbol", _serialize_params(params))
|
|
537
|
+
result = await _ensure_module_and_forward("textDocument/documentSymbol", params, params.text_document.uri)
|
|
502
538
|
if result is None:
|
|
503
539
|
return None
|
|
504
540
|
try:
|
|
@@ -862,6 +862,12 @@ class TestStartPassesEnvToSubprocess:
|
|
|
862
862
|
class TestFindModuleRoot:
|
|
863
863
|
"""Tests for find_module_root — build-file detection for module scoping."""
|
|
864
864
|
|
|
865
|
+
@pytest.fixture(autouse=True)
|
|
866
|
+
def _clear_cache(self) -> None:
|
|
867
|
+
from java_functional_lsp.proxy import _cached_module_root
|
|
868
|
+
|
|
869
|
+
_cached_module_root.cache_clear()
|
|
870
|
+
|
|
865
871
|
def test_finds_pom_xml(self, tmp_path: Any) -> None:
|
|
866
872
|
from java_functional_lsp.proxy import find_module_root
|
|
867
873
|
|
|
@@ -957,7 +963,9 @@ class TestLazyStart:
|
|
|
957
963
|
for i in range(_MAX_QUEUED_NOTIFICATIONS + 50):
|
|
958
964
|
proxy.queue_notification("textDocument/didChange", {"i": i})
|
|
959
965
|
assert len(proxy._queued_notifications) == _MAX_QUEUED_NOTIFICATIONS
|
|
960
|
-
# Oldest entries dropped —
|
|
966
|
+
# Oldest entries dropped — first surviving entry is i=50
|
|
967
|
+
assert proxy._queued_notifications[0] == ("textDocument/didChange", {"i": 50})
|
|
968
|
+
# Last entry is the most recent
|
|
961
969
|
assert proxy._queued_notifications[-1] == ("textDocument/didChange", {"i": _MAX_QUEUED_NOTIFICATIONS + 49})
|
|
962
970
|
|
|
963
971
|
async def test_ensure_started_no_retry_after_failure(self) -> None:
|
|
@@ -998,10 +1006,13 @@ class TestLazyStart:
|
|
|
998
1006
|
java_file.parent.mkdir()
|
|
999
1007
|
java_file.touch()
|
|
1000
1008
|
uri = java_file.as_uri()
|
|
1001
|
-
await proxy.add_module_if_new(uri)
|
|
1009
|
+
result = await proxy.add_module_if_new(uri)
|
|
1010
|
+
assert result is not None # Returns module URI string
|
|
1002
1011
|
proxy.send_notification.assert_called_once() # type: ignore[attr-defined]
|
|
1003
1012
|
call_args = proxy.send_notification.call_args # type: ignore[attr-defined]
|
|
1004
1013
|
assert call_args[0][0] == "workspace/didChangeWorkspaceFolders"
|
|
1014
|
+
# Module should be marked ADDED in registry
|
|
1015
|
+
assert proxy.modules.was_added(result)
|
|
1005
1016
|
|
|
1006
1017
|
async def test_add_module_if_new_skips_duplicate(self) -> None:
|
|
1007
1018
|
from unittest.mock import AsyncMock
|
|
@@ -1020,8 +1031,10 @@ class TestLazyStart:
|
|
|
1020
1031
|
java_file.parent.mkdir()
|
|
1021
1032
|
java_file.touch()
|
|
1022
1033
|
uri = java_file.as_uri()
|
|
1023
|
-
await proxy.add_module_if_new(uri)
|
|
1024
|
-
await proxy.add_module_if_new(uri) # duplicate
|
|
1034
|
+
result1 = await proxy.add_module_if_new(uri)
|
|
1035
|
+
result2 = await proxy.add_module_if_new(uri) # duplicate
|
|
1036
|
+
assert result1 is not None # New module URI
|
|
1037
|
+
assert result2 is None # Already known
|
|
1025
1038
|
assert proxy.send_notification.call_count == 1 # type: ignore[attr-defined]
|
|
1026
1039
|
|
|
1027
1040
|
async def test_expand_full_workspace_sends_notification(self) -> None:
|
|
@@ -1037,6 +1050,25 @@ class TestLazyStart:
|
|
|
1037
1050
|
proxy.send_notification.assert_called_once() # type: ignore[attr-defined]
|
|
1038
1051
|
assert proxy._workspace_expanded is True
|
|
1039
1052
|
|
|
1053
|
+
async def test_expand_full_workspace_removes_initial_module(self) -> None:
|
|
1054
|
+
"""When _initial_module_uri differs from root, it should be in the removed list."""
|
|
1055
|
+
from unittest.mock import AsyncMock
|
|
1056
|
+
|
|
1057
|
+
from java_functional_lsp.proxy import JdtlsProxy
|
|
1058
|
+
|
|
1059
|
+
proxy = JdtlsProxy()
|
|
1060
|
+
proxy._available = True
|
|
1061
|
+
proxy._original_root_uri = "file:///workspace/monorepo"
|
|
1062
|
+
proxy._initial_module_uri = "file:///workspace/monorepo/module-a"
|
|
1063
|
+
proxy.send_notification = AsyncMock() # type: ignore[assignment]
|
|
1064
|
+
await proxy.expand_full_workspace()
|
|
1065
|
+
proxy.send_notification.assert_called_once() # type: ignore[attr-defined]
|
|
1066
|
+
call_args = proxy.send_notification.call_args[0] # type: ignore[attr-defined]
|
|
1067
|
+
event = call_args[1]["event"]
|
|
1068
|
+
assert len(event["removed"]) == 1
|
|
1069
|
+
assert event["removed"][0]["uri"] == "file:///workspace/monorepo/module-a"
|
|
1070
|
+
assert event["added"][0]["uri"] == "file:///workspace/monorepo"
|
|
1071
|
+
|
|
1040
1072
|
async def test_expand_full_workspace_noop_when_not_available(self) -> None:
|
|
1041
1073
|
from unittest.mock import AsyncMock
|
|
1042
1074
|
|
|
@@ -1057,7 +1089,7 @@ class TestLazyStart:
|
|
|
1057
1089
|
proxy = JdtlsProxy()
|
|
1058
1090
|
proxy._available = True
|
|
1059
1091
|
proxy._original_root_uri = "file:///workspace/monorepo"
|
|
1060
|
-
proxy.
|
|
1092
|
+
proxy.modules.mark_added("file:///workspace/monorepo")
|
|
1061
1093
|
proxy.send_notification = AsyncMock() # type: ignore[assignment]
|
|
1062
1094
|
await proxy.expand_full_workspace()
|
|
1063
1095
|
proxy.send_notification.assert_not_called() # type: ignore[attr-defined]
|
|
@@ -476,6 +476,78 @@ class TestServerInternals:
|
|
|
476
476
|
mock_flush.assert_not_called()
|
|
477
477
|
mock_expand.assert_not_called()
|
|
478
478
|
|
|
479
|
+
async def test_ensure_module_and_forward_ready_module_fast_path(self) -> None:
|
|
480
|
+
"""READY module → single send_request, no add_module call."""
|
|
481
|
+
from unittest.mock import AsyncMock, patch
|
|
482
|
+
|
|
483
|
+
from java_functional_lsp.server import _ensure_module_and_forward
|
|
484
|
+
from java_functional_lsp.server import server as srv
|
|
485
|
+
|
|
486
|
+
srv._proxy.modules.mark_added("file:///mod")
|
|
487
|
+
srv._proxy.modules.mark_ready("file:///mod")
|
|
488
|
+
mock_send = AsyncMock(return_value={"result": "ok"})
|
|
489
|
+
try:
|
|
490
|
+
with (
|
|
491
|
+
patch.object(srv._proxy, "send_request", mock_send),
|
|
492
|
+
patch.object(srv._proxy, "_available", True),
|
|
493
|
+
patch("java_functional_lsp.server._resolve_module_uri", return_value="file:///mod"),
|
|
494
|
+
):
|
|
495
|
+
result = await _ensure_module_and_forward("textDocument/hover", {}, "file:///mod/F.java")
|
|
496
|
+
finally:
|
|
497
|
+
srv._proxy.modules.clear()
|
|
498
|
+
assert result == {"result": "ok"}
|
|
499
|
+
assert mock_send.call_count == 1
|
|
500
|
+
|
|
501
|
+
async def test_ensure_module_and_forward_new_module_waits_and_retries(self) -> None:
|
|
502
|
+
"""UNKNOWN module → add, first request null, wait_until_ready, retry succeeds."""
|
|
503
|
+
from unittest.mock import AsyncMock, patch
|
|
504
|
+
|
|
505
|
+
from java_functional_lsp.server import _ensure_module_and_forward
|
|
506
|
+
from java_functional_lsp.server import server as srv
|
|
507
|
+
|
|
508
|
+
mock_add = AsyncMock(return_value="file:///mod")
|
|
509
|
+
mock_send = AsyncMock(side_effect=[None, {"result": "ok"}])
|
|
510
|
+
|
|
511
|
+
async def mock_wait(uri: str, timeout: float = 30.0) -> bool:
|
|
512
|
+
srv._proxy.modules.mark_ready(uri)
|
|
513
|
+
return True
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
with (
|
|
517
|
+
patch.object(srv._proxy, "add_module_if_new", mock_add),
|
|
518
|
+
patch.object(srv._proxy, "send_request", mock_send),
|
|
519
|
+
patch.object(srv._proxy, "_available", True),
|
|
520
|
+
patch.object(srv._proxy.modules, "wait_until_ready", mock_wait),
|
|
521
|
+
patch("java_functional_lsp.server._resolve_module_uri", return_value="file:///mod"),
|
|
522
|
+
):
|
|
523
|
+
result = await _ensure_module_and_forward("textDocument/hover", {}, "file:///mod/F.java")
|
|
524
|
+
finally:
|
|
525
|
+
srv._proxy.modules.clear()
|
|
526
|
+
assert result == {"result": "ok"}
|
|
527
|
+
assert mock_send.call_count == 2
|
|
528
|
+
|
|
529
|
+
async def test_ensure_module_and_forward_success_marks_ready(self) -> None:
|
|
530
|
+
"""First successful request marks module as READY."""
|
|
531
|
+
from unittest.mock import AsyncMock, patch
|
|
532
|
+
|
|
533
|
+
from java_functional_lsp.proxy import ModuleState
|
|
534
|
+
from java_functional_lsp.server import _ensure_module_and_forward
|
|
535
|
+
from java_functional_lsp.server import server as srv
|
|
536
|
+
|
|
537
|
+
mock_add = AsyncMock(return_value="file:///mod")
|
|
538
|
+
mock_send = AsyncMock(return_value={"result": "ok"})
|
|
539
|
+
try:
|
|
540
|
+
with (
|
|
541
|
+
patch.object(srv._proxy, "add_module_if_new", mock_add),
|
|
542
|
+
patch.object(srv._proxy, "send_request", mock_send),
|
|
543
|
+
patch.object(srv._proxy, "_available", True),
|
|
544
|
+
patch("java_functional_lsp.server._resolve_module_uri", return_value="file:///mod"),
|
|
545
|
+
):
|
|
546
|
+
await _ensure_module_and_forward("textDocument/hover", {}, "file:///mod/F.java")
|
|
547
|
+
assert srv._proxy.modules.get_state("file:///mod") == ModuleState.READY
|
|
548
|
+
finally:
|
|
549
|
+
srv._proxy.modules.clear()
|
|
550
|
+
|
|
479
551
|
def test_serialize_params_camelcase(self) -> None:
|
|
480
552
|
from java_functional_lsp.server import _serialize_params
|
|
481
553
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.github/ISSUE_TEMPLATE/bug-report.md
RENAMED
|
File without changes
|
{java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.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.7.4 → java_functional_lsp-0.7.5}/.github/workflows/release-drafter.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/.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
|
|
File without changes
|
{java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/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
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.4 → java_functional_lsp-0.7.5}/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
|