java-functional-lsp 0.9.0__tar.gz → 0.9.2__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 (69) hide show
  1. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/PKG-INFO +1 -1
  2. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/pyproject.toml +1 -1
  3. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/__init__.py +1 -1
  4. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/proxy.py +72 -10
  5. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/server.py +12 -3
  6. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_proxy.py +67 -3
  7. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.claude-plugin/plugin.json +0 -0
  8. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.githooks/pre-commit +0 -0
  9. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.githooks/pre-push +0 -0
  10. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/CODEOWNERS +0 -0
  11. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  12. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  13. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/SECURITY.md +0 -0
  15. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/dependabot.yml +0 -0
  16. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/release-drafter.yml +0 -0
  17. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/workflows/publish.yml +0 -0
  18. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/workflows/release-drafter.yml +0 -0
  19. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/workflows/stale.yml +0 -0
  20. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/workflows/test.yml +0 -0
  21. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/workflows/update-homebrew.yml +0 -0
  22. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.github/workflows/vscode-ext.yml +0 -0
  23. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/.gitignore +0 -0
  24. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/CONTRIBUTING.md +0 -0
  25. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/LICENSE +0 -0
  26. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/README.md +0 -0
  27. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/SKILL.md +0 -0
  28. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/commands/lint-java.md +0 -0
  29. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/intellij/README.md +0 -0
  30. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/intellij/lsp4ij-template.json +0 -0
  31. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/vscode/.vscodeignore +0 -0
  32. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/vscode/README.md +0 -0
  33. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/vscode/package-lock.json +0 -0
  34. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/vscode/package.json +0 -0
  35. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/vscode/src/extension.ts +0 -0
  36. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/editors/vscode/tsconfig.json +0 -0
  37. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/hooks/hooks.json +0 -0
  38. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/hooks/java_linter_reminder.py +0 -0
  39. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/scripts/ensure-lsp.sh +0 -0
  40. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/scripts/generate-formula.py +0 -0
  41. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/__main__.py +0 -0
  42. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  43. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/analyzers/base.py +0 -0
  44. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  45. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  46. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  47. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  48. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  49. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/cli.py +0 -0
  50. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/fixes.py +0 -0
  51. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/src/java_functional_lsp/merkle.py +0 -0
  52. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/__init__.py +0 -0
  53. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/conftest.py +0 -0
  54. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_base.py +0 -0
  55. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_cli.py +0 -0
  56. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_config.py +0 -0
  57. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_e2e.py +0 -0
  58. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_e2e_jdtls.py +0 -0
  59. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_exception_checker.py +0 -0
  60. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_fixes.py +0 -0
  61. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_functional_checker.py +0 -0
  62. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_merkle.py +0 -0
  63. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_merkle_proxy.py +0 -0
  64. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_mutation_checker.py +0 -0
  65. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_null_checker.py +0 -0
  66. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_server.py +0 -0
  67. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_spring_checker.py +0 -0
  68. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/tests/test_suppress.py +0 -0
  69. {java_functional_lsp-0.9.0 → java_functional_lsp-0.9.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.9.0
3
+ Version: 0.9.2
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.9.0"
7
+ version = "0.9.2"
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.9.0"
3
+ __version__ = "0.9.2"
@@ -445,6 +445,42 @@ def _version_key(name: str) -> tuple[int, ...]:
445
445
  return tuple(parts)
446
446
 
447
447
 
448
+ def _find_maven_group_root(initial_module: Path, workspace_root: Path) -> Path:
449
+ """Return the tightest Maven multi-module parent of *initial_module*.
450
+
451
+ Walks up from ``initial_module.parent`` (the module itself is skipped —
452
+ we want its *parent* group, not the module's own pom) toward (and
453
+ including) ``workspace_root``, returning the first ancestor whose
454
+ ``pom.xml`` contains a ``<modules>`` section. Falls back to
455
+ ``workspace_root`` when no such intermediate parent exists (e.g. flat
456
+ single-level monorepos).
457
+
458
+ This keeps the jdtls index bounded to a module *group* (typically
459
+ 5-20 modules) rather than the full IDE workspace (potentially 200+ modules)
460
+ which would cause ``OutOfMemoryError: Java heap space`` during indexing.
461
+
462
+ Both *initial_module* and *workspace_root* are resolved to real paths
463
+ before walking to avoid symlink-based boundary escapes.
464
+ """
465
+ workspace_root = workspace_root.resolve()
466
+ current = initial_module.resolve().parent
467
+ # Bail immediately if the initial module is outside the workspace.
468
+ if not current.is_relative_to(workspace_root):
469
+ return workspace_root
470
+ while True:
471
+ pom = current / "pom.xml"
472
+ if pom.is_file():
473
+ try:
474
+ if "<modules>" in pom.read_text(encoding="utf-8", errors="ignore"):
475
+ return current
476
+ except OSError:
477
+ pass
478
+ if current in (workspace_root, current.parent):
479
+ break
480
+ current = current.parent
481
+ return workspace_root
482
+
483
+
448
484
  @lru_cache(maxsize=256)
449
485
  def _module_snapshot_path(module_uri: str) -> Path:
450
486
  """Return the path for the persistent snapshot file for *module_uri*.
@@ -777,6 +813,11 @@ class JdtlsProxy:
777
813
  Uses a ``deque(maxlen=200)`` so oldest entries are dropped in O(1)
778
814
  when the queue overflows during long jdtls startup.
779
815
  """
816
+ if len(self._queued_notifications) >= _MAX_QUEUED_NOTIFICATIONS:
817
+ logger.warning(
818
+ "jdtls: notification queue full (%d cap) — oldest entry dropped; some didOpen events may be missed",
819
+ _MAX_QUEUED_NOTIFICATIONS,
820
+ )
780
821
  self._queued_notifications.append((method, params))
781
822
 
782
823
  async def flush_queued_notifications(self) -> None:
@@ -832,7 +873,7 @@ class JdtlsProxy:
832
873
  Calls ``modules.mark_added()`` before any ``await`` to prevent
833
874
  duplicate add calls from concurrent coroutines.
834
875
  """
835
- if not self._available:
876
+ if not self._available or self._workspace_expanded:
836
877
  return None
837
878
  module_uri = _resolve_module_uri(file_uri)
838
879
  if module_uri is None or self.modules.was_added(module_uri):
@@ -857,29 +898,50 @@ class JdtlsProxy:
857
898
  return module_uri
858
899
 
859
900
  async def expand_full_workspace(self) -> None:
860
- """Expand jdtls workspace to the full monorepo root (background task).
901
+ """Expand jdtls workspace to the nearest Maven module-group root.
902
+
903
+ Instead of expanding all the way to the IDE workspace root (which may
904
+ contain hundreds of Maven modules and cause jdtls OOM), this walks up
905
+ from the initial module to find the *nearest* ancestor directory whose
906
+ ``pom.xml`` declares ``<modules>`` — i.e. the tightest multi-module
907
+ Maven parent. This covers sibling modules (the common case for
908
+ cross-module navigation) without ballooning the index.
909
+
910
+ Falls back to the full IDE workspace root when no intermediate
911
+ multi-module parent is found.
861
912
 
862
- Removes the initial module-scoped folder and adds the full root to
863
- avoid double-indexing.
913
+ Removes all individually-added module folders to avoid double-indexing
914
+ with the newly added group root.
864
915
  """
865
916
  if self._workspace_expanded or not self._available or not self._original_root_uri:
866
917
  return
867
918
  from pygls.uris import from_fs_path, to_fs_path
868
919
 
869
- root_path = to_fs_path(self._original_root_uri) or self._original_root_uri
920
+ workspace_path = to_fs_path(self._original_root_uri) or self._original_root_uri
921
+
922
+ # Find the tightest multi-module Maven parent of the initial module.
923
+ group_root_path: str = workspace_path
924
+ if self._initial_module_uri:
925
+ initial_path = to_fs_path(self._initial_module_uri) or self._initial_module_uri
926
+ group_root_path = str(_find_maven_group_root(Path(initial_path), Path(workspace_path)))
927
+
928
+ root_path = group_root_path
870
929
  root_uri = from_fs_path(root_path) or self._original_root_uri
871
930
  if self.modules.was_added(root_uri):
872
931
  self._workspace_expanded = True
873
932
  return
874
933
  self.modules.mark_added(root_uri)
875
934
 
876
- # Remove initial module folder to avoid double-indexing.
935
+ # Remove ALL individually-added modules to prevent double-indexing with
936
+ # the full root. This covers both the initial scoped module and any
937
+ # modules added by add_module_if_new during the ensure_started window.
877
938
  removed: list[dict[str, str]] = []
878
- if self._initial_module_uri and self._initial_module_uri != root_uri:
879
- ini_path = to_fs_path(self._initial_module_uri) or self._initial_module_uri
880
- removed.append({"uri": self._initial_module_uri, "name": Path(ini_path).name})
939
+ for uri in list(self.modules._states):
940
+ if uri != root_uri:
941
+ p = to_fs_path(uri) or uri
942
+ removed.append({"uri": uri, "name": Path(p).name})
881
943
 
882
- logger.info("jdtls: expanding to full workspace %s", _redact_path(root_path))
944
+ logger.info("jdtls: expanding workspace to module group %s", _redact_path(root_path))
883
945
  await self.send_notification(
884
946
  _WORKSPACE_DID_CHANGE_FOLDERS,
885
947
  {"event": {"added": [{"uri": root_uri, "name": Path(root_path).name}], "removed": removed}},
@@ -98,6 +98,8 @@ class JavaFunctionalLspServer(LanguageServer):
98
98
  _fire_and_forget(_apply_module_diff(self._proxy, module_uri))
99
99
  try:
100
100
  _analyze_and_publish(uri)
101
+ except FileNotFoundError:
102
+ pass # Virtual jdtls file (e.g., decompiled source in jdtls-data); skip silently.
101
103
  except Exception as e:
102
104
  logger.error("Error re-publishing diagnostics for %s: %s", uri, e)
103
105
 
@@ -602,14 +604,21 @@ async def _lazy_start_jdtls(file_uri: str) -> None:
602
604
  """Background task: start jdtls scoped to the module containing *file_uri*.
603
605
 
604
606
  Runs in the background so ``on_did_open`` returns immediately with custom
605
- diagnostics. After jdtls initializes, registers capabilities and flushes
606
- queued notifications. Workspace expansion is NOT done eagerly — modules
607
- are loaded on-demand via ``add_module_if_new()`` as files are opened.
607
+ diagnostics. After jdtls initializes, the workspace is immediately expanded
608
+ to the full IDE workspace root (inside-out) so jdtls discovers all sibling
609
+ modules before any queued ``didOpen`` notifications are flushed. This means
610
+ ``add_module_if_new`` is a no-op for all subsequent file opens — jdtls
611
+ already covers the whole project.
608
612
  """
609
613
  try:
610
614
  started = await server._proxy.ensure_started(server._init_params, file_uri, config=server._config)
611
615
  if started:
612
616
  logger.info("jdtls proxy active — full Java language support enabled")
617
+ # Expand to full workspace BEFORE flushing queued notifications so
618
+ # all files are processed in the full-workspace context (inside-out).
619
+ # expand_full_workspace is a no-op for standalone projects where
620
+ # the workspace root equals the initial module.
621
+ await server._proxy.expand_full_workspace()
613
622
  if server._skip_jdtls_registration:
614
623
  logger.info("Skipping dynamic capability registration (no-register mode)")
615
624
  else:
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import subprocess
8
+ from pathlib import Path
8
9
  from typing import Any
9
10
  from unittest.mock import MagicMock, patch
10
11
 
@@ -1037,6 +1038,65 @@ class TestLazyStart:
1037
1038
  assert result2 is None # Already known
1038
1039
  assert proxy.send_notification.call_count == 1 # type: ignore[attr-defined]
1039
1040
 
1041
+ async def test_expand_uses_maven_group_root_not_workspace_root(self, tmp_path: Path) -> None:
1042
+ """expand_full_workspace expands to nearest multi-module Maven parent, not the full IDE root."""
1043
+ from unittest.mock import AsyncMock
1044
+
1045
+ from java_functional_lsp.proxy import JdtlsProxy
1046
+
1047
+ # Layout: workspace/group/module — group/pom.xml has <modules>
1048
+ workspace = tmp_path / "workspace"
1049
+ group = workspace / "group"
1050
+ module = group / "module"
1051
+ module.mkdir(parents=True)
1052
+ (group / "pom.xml").write_text("<project><modules><module>module</module></modules></project>")
1053
+ (workspace / "pom.xml").write_text("<project><modules><module>group</module></modules></project>")
1054
+
1055
+ module_uri = module.as_uri()
1056
+ workspace_uri = workspace.as_uri()
1057
+
1058
+ proxy = JdtlsProxy()
1059
+ proxy._available = True
1060
+ proxy._original_root_uri = workspace_uri
1061
+ proxy._initial_module_uri = module_uri
1062
+ proxy.modules.mark_added(module_uri)
1063
+ proxy.send_notification = AsyncMock() # type: ignore[assignment]
1064
+ await proxy.expand_full_workspace()
1065
+
1066
+ call_args = proxy.send_notification.call_args[0] # type: ignore[attr-defined]
1067
+ event = call_args[1]["event"]
1068
+ # Should expand to group/, NOT workspace/
1069
+ assert event["added"][0]["uri"] == group.as_uri()
1070
+ assert event["added"][0]["uri"] != workspace_uri
1071
+
1072
+ async def test_expand_falls_back_to_workspace_root_when_no_group_pom(self, tmp_path: Path) -> None:
1073
+ """Falls back to the IDE workspace root when no intermediate multi-module parent exists."""
1074
+ from unittest.mock import AsyncMock
1075
+
1076
+ from java_functional_lsp.proxy import JdtlsProxy
1077
+
1078
+ # Flat layout: workspace/module — no intermediate pom.xml
1079
+ workspace = tmp_path / "workspace"
1080
+ module = workspace / "module"
1081
+ module.mkdir(parents=True)
1082
+ # workspace/pom.xml has <modules>, but there's nothing in between
1083
+ (workspace / "pom.xml").write_text("<project><modules><module>module</module></modules></project>")
1084
+
1085
+ module_uri = module.as_uri()
1086
+ workspace_uri = workspace.as_uri()
1087
+
1088
+ proxy = JdtlsProxy()
1089
+ proxy._available = True
1090
+ proxy._original_root_uri = workspace_uri
1091
+ proxy._initial_module_uri = module_uri
1092
+ proxy.modules.mark_added(module_uri)
1093
+ proxy.send_notification = AsyncMock() # type: ignore[assignment]
1094
+ await proxy.expand_full_workspace()
1095
+
1096
+ call_args = proxy.send_notification.call_args[0] # type: ignore[attr-defined]
1097
+ event = call_args[1]["event"]
1098
+ assert event["added"][0]["uri"] == workspace_uri
1099
+
1040
1100
  async def test_expand_full_workspace_sends_notification(self) -> None:
1041
1101
  from unittest.mock import AsyncMock
1042
1102
 
@@ -1051,7 +1111,7 @@ class TestLazyStart:
1051
1111
  assert proxy._workspace_expanded is True
1052
1112
 
1053
1113
  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."""
1114
+ """All modules in _states (not just _initial_module_uri) are removed on expansion."""
1055
1115
  from unittest.mock import AsyncMock
1056
1116
 
1057
1117
  from java_functional_lsp.proxy import JdtlsProxy
@@ -1060,13 +1120,17 @@ class TestLazyStart:
1060
1120
  proxy._available = True
1061
1121
  proxy._original_root_uri = "file:///workspace/monorepo"
1062
1122
  proxy._initial_module_uri = "file:///workspace/monorepo/module-a"
1123
+ # Simulate that module-a was mark_added during start() and module-b via add_module_if_new
1124
+ proxy.modules.mark_added("file:///workspace/monorepo/module-a")
1125
+ proxy.modules.mark_added("file:///workspace/monorepo/module-b")
1063
1126
  proxy.send_notification = AsyncMock() # type: ignore[assignment]
1064
1127
  await proxy.expand_full_workspace()
1065
1128
  proxy.send_notification.assert_called_once() # type: ignore[attr-defined]
1066
1129
  call_args = proxy.send_notification.call_args[0] # type: ignore[attr-defined]
1067
1130
  event = call_args[1]["event"]
1068
- assert len(event["removed"]) == 1
1069
- assert event["removed"][0]["uri"] == "file:///workspace/monorepo/module-a"
1131
+ removed_uris = {e["uri"] for e in event["removed"]}
1132
+ assert "file:///workspace/monorepo/module-a" in removed_uris
1133
+ assert "file:///workspace/monorepo/module-b" in removed_uris
1070
1134
  assert event["added"][0]["uri"] == "file:///workspace/monorepo"
1071
1135
 
1072
1136
  async def test_expand_full_workspace_noop_when_not_available(self) -> None: