java-functional-lsp 0.7.6__tar.gz → 0.7.8__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.
Files changed (66) hide show
  1. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/PKG-INFO +1 -1
  2. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/pyproject.toml +1 -1
  3. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/__init__.py +1 -1
  4. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/proxy.py +25 -7
  5. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/server.py +12 -4
  6. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_server.py +1 -4
  7. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.claude-plugin/plugin.json +0 -0
  8. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.githooks/pre-commit +0 -0
  9. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.githooks/pre-push +0 -0
  10. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/CODEOWNERS +0 -0
  11. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  12. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  13. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/SECURITY.md +0 -0
  15. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/dependabot.yml +0 -0
  16. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/release-drafter.yml +0 -0
  17. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/workflows/publish.yml +0 -0
  18. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/workflows/release-drafter.yml +0 -0
  19. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/workflows/stale.yml +0 -0
  20. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/workflows/test.yml +0 -0
  21. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/workflows/update-homebrew.yml +0 -0
  22. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.github/workflows/vscode-ext.yml +0 -0
  23. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/.gitignore +0 -0
  24. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/CONTRIBUTING.md +0 -0
  25. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/LICENSE +0 -0
  26. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/README.md +0 -0
  27. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/SKILL.md +0 -0
  28. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/commands/lint-java.md +0 -0
  29. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/intellij/README.md +0 -0
  30. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/intellij/lsp4ij-template.json +0 -0
  31. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/vscode/.vscodeignore +0 -0
  32. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/vscode/README.md +0 -0
  33. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/vscode/package-lock.json +0 -0
  34. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/vscode/package.json +0 -0
  35. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/vscode/src/extension.ts +0 -0
  36. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/editors/vscode/tsconfig.json +0 -0
  37. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/hooks/hooks.json +0 -0
  38. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/hooks/java_linter_reminder.py +0 -0
  39. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/scripts/ensure-lsp.sh +0 -0
  40. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/scripts/generate-formula.py +0 -0
  41. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/__main__.py +0 -0
  42. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  43. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/analyzers/base.py +0 -0
  44. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  45. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  46. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  47. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  48. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  49. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/cli.py +0 -0
  50. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/src/java_functional_lsp/fixes.py +0 -0
  51. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/__init__.py +0 -0
  52. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/conftest.py +0 -0
  53. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_base.py +0 -0
  54. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_cli.py +0 -0
  55. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_config.py +0 -0
  56. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_e2e.py +0 -0
  57. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_e2e_jdtls.py +0 -0
  58. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_exception_checker.py +0 -0
  59. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_fixes.py +0 -0
  60. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_functional_checker.py +0 -0
  61. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_mutation_checker.py +0 -0
  62. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_null_checker.py +0 -0
  63. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_proxy.py +0 -0
  64. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_spring_checker.py +0 -0
  65. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/tests/test_suppress.py +0 -0
  66. {java_functional_lsp-0.7.6 → java_functional_lsp-0.7.8}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.7.6
3
+ Version: 0.7.8
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.6"
7
+ version = "0.7.8"
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" }
@@ -1,3 +1,3 @@
1
1
  """java-functional-lsp: A Java LSP server enforcing functional programming best practices."""
2
2
 
3
- __version__ = "0.7.6"
3
+ __version__ = "0.7.8"
@@ -12,6 +12,7 @@ import platform
12
12
  import re
13
13
  import shutil
14
14
  import subprocess
15
+ import time
15
16
  from collections import deque
16
17
  from collections.abc import Callable, Mapping
17
18
  from functools import lru_cache
@@ -22,6 +23,7 @@ logger = logging.getLogger(__name__)
22
23
 
23
24
  REQUEST_TIMEOUT = 30.0 # seconds — per-request timeout for normal operations
24
25
  _INITIALIZE_TIMEOUT = 120.0 # seconds — module-scoped init can still be slow (Maven classpath resolution)
26
+ _START_RETRY_COOLDOWN = 300.0 # seconds — retry jdtls startup after transient failure
25
27
  DEFAULT_JVM_MAX_HEAP = "4g"
26
28
  _STDERR_LINE_MAX = 1000
27
29
 
@@ -448,6 +450,7 @@ class JdtlsProxy:
448
450
  self._start_lock = asyncio.Lock()
449
451
  self._starting = False
450
452
  self._start_failed = False
453
+ self._start_failed_at: float | None = None
451
454
  self._jdtls_on_path = False
452
455
  self._lazy_start_fired = False
453
456
  self._queued_notifications: deque[tuple[str, Any]] = deque(maxlen=_MAX_QUEUED_NOTIFICATIONS)
@@ -481,18 +484,22 @@ class JdtlsProxy:
481
484
  """Start jdtls subprocess and initialize it.
482
485
 
483
486
  If *module_root_uri* is provided, jdtls is scoped to that module for
484
- fast startup. The data-directory hash is always based on the original
485
- workspace root (from init_params) so the index persists across restarts.
487
+ fast startup and the data-dir hash is based on the module URI (so each
488
+ module gets its own isolated index). Otherwise the workspace root is used.
486
489
  """
487
490
  jdtls_path = shutil.which("jdtls")
488
491
  if not jdtls_path:
489
492
  return False
490
493
 
491
- # Data-dir hash based on original workspace root (stable across module changes).
494
+ # Data-dir hash: use module URI when scoped so each module gets its own
495
+ # clean jdtls state. This avoids loading a 2.5GB monorepo index when only
496
+ # one module is needed. Without expansion, each session is module-scoped,
497
+ # so the data-dir should match that scope.
492
498
  original_root: str = init_params.get("rootUri") or init_params.get("rootPath") or str(Path.cwd())
493
499
  self._original_root_uri = original_root
494
- workspace_hash = hashlib.sha256(original_root.encode()).hexdigest()[:12]
495
- data_dir = Path.home() / ".cache" / "jdtls-data" / workspace_hash
500
+ hash_source = module_root_uri or original_root
501
+ data_hash = hashlib.sha256(hash_source.encode()).hexdigest()[:12]
502
+ data_dir = Path.home() / ".cache" / "jdtls-data" / data_hash
496
503
  data_dir.mkdir(parents=True, exist_ok=True)
497
504
 
498
505
  # Deep copy to avoid mutating server._init_params.
@@ -566,12 +573,21 @@ class JdtlsProxy:
566
573
  """Start jdtls lazily, scoped to the module containing *file_uri*.
567
574
 
568
575
  Thread-safe: uses asyncio.Lock to prevent double-start from rapid
569
- didOpen calls. Sets ``_start_failed`` on failure to prevent retries.
576
+ didOpen calls. Sets ``_start_failed`` on failure to prevent retries,
577
+ but allows retry after a cooldown period (5 minutes) so transient
578
+ failures (Maven Central timeout, JVM OOM) don't permanently disable jdtls.
570
579
  """
571
580
  if self._available:
572
581
  return True
573
- if self._start_failed or not self._jdtls_on_path:
582
+ if not self._jdtls_on_path:
574
583
  return False
584
+ if self._start_failed:
585
+ if self._start_failed_at and (time.monotonic() - self._start_failed_at > _START_RETRY_COOLDOWN):
586
+ self._start_failed = False
587
+ self._start_failed_at = None
588
+ logger.info("jdtls: retrying after previous failure (cooldown elapsed)")
589
+ else:
590
+ return False
575
591
 
576
592
  async with self._start_lock:
577
593
  if self._available:
@@ -583,10 +599,12 @@ class JdtlsProxy:
583
599
  started = await self.start(init_params, module_root_uri=module_uri)
584
600
  if not started:
585
601
  self._start_failed = True
602
+ self._start_failed_at = time.monotonic()
586
603
  self._queued_notifications.clear()
587
604
  return started
588
605
  except Exception:
589
606
  self._start_failed = True
607
+ self._start_failed_at = time.monotonic()
590
608
  self._queued_notifications.clear()
591
609
  raise
592
610
  finally:
@@ -66,9 +66,17 @@ class JavaFunctionalLspServer(LanguageServer):
66
66
  self._proxy = JdtlsProxy(on_diagnostics=self._on_jdtls_diagnostics)
67
67
 
68
68
  def _on_jdtls_diagnostics(self, uri: str, diagnostics: list[Any]) -> None:
69
- """Called when jdtls publishes diagnostics — merge with custom and re-publish."""
69
+ """Called when jdtls publishes diagnostics — merge with custom and re-publish.
70
+
71
+ Also marks the file's module as READY, since receiving diagnostics from
72
+ jdtls is a reliable signal that the module has been indexed (more reliable
73
+ than a first non-None response which may be semantically empty).
74
+ """
70
75
  if not uri.endswith(".java"):
71
76
  return
77
+ module_uri = _resolve_module_uri(uri)
78
+ if module_uri:
79
+ self._proxy.modules.mark_ready(module_uri)
72
80
  try:
73
81
  _analyze_and_publish(uri)
74
82
  except Exception as e:
@@ -406,8 +414,9 @@ async def _lazy_start_jdtls(file_uri: str) -> None:
406
414
  """Background task: start jdtls scoped to the module containing *file_uri*.
407
415
 
408
416
  Runs in the background so ``on_did_open`` returns immediately with custom
409
- diagnostics. After jdtls initializes, registers capabilities, flushes
410
- queued notifications, and schedules workspace expansion.
417
+ diagnostics. After jdtls initializes, registers capabilities and flushes
418
+ queued notifications. Workspace expansion is NOT done eagerly — modules
419
+ are loaded on-demand via ``add_module_if_new()`` as files are opened.
411
420
  """
412
421
  try:
413
422
  started = await server._proxy.ensure_started(server._init_params, file_uri)
@@ -415,7 +424,6 @@ async def _lazy_start_jdtls(file_uri: str) -> None:
415
424
  logger.info("jdtls proxy active — full Java language support enabled")
416
425
  await _register_jdtls_capabilities()
417
426
  await server._proxy.flush_queued_notifications()
418
- await _expand_workspace_background()
419
427
  except Exception:
420
428
  logger.warning("jdtls lazy start failed", exc_info=True)
421
429
 
@@ -415,7 +415,7 @@ class TestServerInternals:
415
415
  mock_reg.assert_not_called()
416
416
 
417
417
  async def test_lazy_start_jdtls_success(self, caplog: Any) -> None:
418
- """_lazy_start_jdtls logs success, flushes queue, and expands workspace."""
418
+ """_lazy_start_jdtls logs success and flushes queue (no eager expansion)."""
419
419
  import logging
420
420
  from unittest.mock import AsyncMock, MagicMock, patch
421
421
 
@@ -424,7 +424,6 @@ class TestServerInternals:
424
424
  from java_functional_lsp.server import server as srv
425
425
 
426
426
  mock_flush = AsyncMock()
427
- mock_expand = AsyncMock()
428
427
  old_flag = srv_mod._jdtls_capabilities_registered
429
428
  srv_mod._jdtls_capabilities_registered = False
430
429
  try:
@@ -432,7 +431,6 @@ class TestServerInternals:
432
431
  caplog.at_level(logging.INFO, logger="java_functional_lsp.server"),
433
432
  patch.object(srv._proxy, "ensure_started", AsyncMock(return_value=True)),
434
433
  patch.object(srv._proxy, "flush_queued_notifications", mock_flush),
435
- patch.object(srv._proxy, "expand_full_workspace", mock_expand),
436
434
  patch.object(srv, "feature", MagicMock(return_value=lambda fn: fn)),
437
435
  patch.object(srv, "client_register_capability_async", AsyncMock()),
438
436
  ):
@@ -441,7 +439,6 @@ class TestServerInternals:
441
439
  srv_mod._jdtls_capabilities_registered = old_flag
442
440
  assert any("jdtls proxy active" in r.getMessage() for r in caplog.records)
443
441
  mock_flush.assert_called_once()
444
- mock_expand.assert_called_once()
445
442
 
446
443
  async def test_lazy_start_jdtls_failure_logged(self, caplog: Any) -> None:
447
444
  """_lazy_start_jdtls logs warning on exception."""