java-functional-lsp 0.9.2__tar.gz → 0.9.4__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.2 → java_functional_lsp-0.9.4}/PKG-INFO +1 -1
  2. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/pyproject.toml +1 -1
  3. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/__init__.py +1 -1
  4. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/proxy.py +39 -14
  5. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_proxy.py +160 -19
  6. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.claude-plugin/plugin.json +0 -0
  7. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.githooks/pre-commit +0 -0
  8. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.githooks/pre-push +0 -0
  9. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/CODEOWNERS +0 -0
  10. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  11. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  12. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/SECURITY.md +0 -0
  14. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/dependabot.yml +0 -0
  15. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/release-drafter.yml +0 -0
  16. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/workflows/publish.yml +0 -0
  17. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/workflows/release-drafter.yml +0 -0
  18. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/workflows/stale.yml +0 -0
  19. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/workflows/test.yml +0 -0
  20. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/workflows/update-homebrew.yml +0 -0
  21. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.github/workflows/vscode-ext.yml +0 -0
  22. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/.gitignore +0 -0
  23. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/CONTRIBUTING.md +0 -0
  24. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/LICENSE +0 -0
  25. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/README.md +0 -0
  26. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/SKILL.md +0 -0
  27. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/commands/lint-java.md +0 -0
  28. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/intellij/README.md +0 -0
  29. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/intellij/lsp4ij-template.json +0 -0
  30. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/vscode/.vscodeignore +0 -0
  31. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/vscode/README.md +0 -0
  32. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/vscode/package-lock.json +0 -0
  33. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/vscode/package.json +0 -0
  34. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/vscode/src/extension.ts +0 -0
  35. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/editors/vscode/tsconfig.json +0 -0
  36. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/hooks/hooks.json +0 -0
  37. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/hooks/java_linter_reminder.py +0 -0
  38. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/scripts/ensure-lsp.sh +0 -0
  39. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/scripts/generate-formula.py +0 -0
  40. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/__main__.py +0 -0
  41. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  42. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/analyzers/base.py +0 -0
  43. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  44. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  45. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  46. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  47. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  48. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/cli.py +0 -0
  49. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/fixes.py +0 -0
  50. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/merkle.py +0 -0
  51. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/src/java_functional_lsp/server.py +0 -0
  52. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/__init__.py +0 -0
  53. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/conftest.py +0 -0
  54. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_base.py +0 -0
  55. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_cli.py +0 -0
  56. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_config.py +0 -0
  57. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_e2e.py +0 -0
  58. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_e2e_jdtls.py +0 -0
  59. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_exception_checker.py +0 -0
  60. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_fixes.py +0 -0
  61. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_functional_checker.py +0 -0
  62. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_merkle.py +0 -0
  63. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_merkle_proxy.py +0 -0
  64. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_mutation_checker.py +0 -0
  65. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_null_checker.py +0 -0
  66. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_server.py +0 -0
  67. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_spring_checker.py +0 -0
  68. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/tests/test_suppress.py +0 -0
  69. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.9.2
3
+ Version: 0.9.4
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.2"
7
+ version = "0.9.4"
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.2"
3
+ __version__ = "0.9.4"
@@ -448,12 +448,12 @@ def _version_key(name: str) -> tuple[int, ...]:
448
448
  def _find_maven_group_root(initial_module: Path, workspace_root: Path) -> Path:
449
449
  """Return the tightest Maven multi-module parent of *initial_module*.
450
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).
451
+ Walks up from ``initial_module.parent`` toward (but NOT including)
452
+ ``workspace_root``, returning the first ancestor whose ``pom.xml``
453
+ contains a ``<modules>`` section. Falls back to ``initial_module``
454
+ itself when no intermediate group pom exists (e.g. modules that sit
455
+ directly under ``workspace_root``), keeping the scope as tight as
456
+ possible and avoiding accidental full-monorepo indexing.
457
457
 
458
458
  This keeps the jdtls index bounded to a module *group* (typically
459
459
  5-20 modules) rather than the full IDE workspace (potentially 200+ modules)
@@ -463,11 +463,17 @@ def _find_maven_group_root(initial_module: Path, workspace_root: Path) -> Path:
463
463
  before walking to avoid symlink-based boundary escapes.
464
464
  """
465
465
  workspace_root = workspace_root.resolve()
466
- current = initial_module.resolve().parent
467
- # Bail immediately if the initial module is outside the workspace.
466
+ resolved_module = initial_module.resolve()
467
+ current = resolved_module.parent
468
+ # Case 1: module is outside the workspace entirely — scope to workspace_root
469
+ # (also fires when initial_module == workspace_root, since parent is then
470
+ # outside workspace_root).
468
471
  if not current.is_relative_to(workspace_root):
469
472
  return workspace_root
470
- while True:
473
+ # Walk up to (but NOT including) workspace_root. Inspecting workspace_root's
474
+ # own pom.xml would include the entire monorepo (e.g. 200+ modules) → OOM.
475
+ # `current.parent == current` detects the filesystem root (POSIX `/` or Windows drive).
476
+ while current not in (workspace_root, current.parent):
471
477
  pom = current / "pom.xml"
472
478
  if pom.is_file():
473
479
  try:
@@ -475,10 +481,10 @@ def _find_maven_group_root(initial_module: Path, workspace_root: Path) -> Path:
475
481
  return current
476
482
  except OSError:
477
483
  pass
478
- if current in (workspace_root, current.parent):
479
- break
480
484
  current = current.parent
481
- return workspace_root
485
+ # Case 2: no intermediate group pom found — use the initial module as its own scope
486
+ # (tightest possible; avoids full-monorepo indexing for flat modules under workspace_root).
487
+ return resolved_module
482
488
 
483
489
 
484
490
  @lru_cache(maxsize=256)
@@ -530,6 +536,22 @@ def _compute_module_diff(
530
536
  return (diff, current)
531
537
 
532
538
 
539
+ def _wipe_data_dir(data_dir: Path) -> None:
540
+ """Remove *data_dir* so the next jdtls startup gets a clean cold start.
541
+
542
+ Called after an initialize timeout to recover from a corrupted OSGi
543
+ data-dir (e.g. left behind after an OOM crash).
544
+ """
545
+ shutil.rmtree(data_dir, ignore_errors=True)
546
+ if data_dir.exists():
547
+ logger.warning(
548
+ "jdtls: failed to wipe data-dir %s — next retry may hang again",
549
+ _redact_path(str(data_dir)),
550
+ )
551
+ else:
552
+ logger.info("jdtls: wiped data-dir %s after init timeout", _redact_path(str(data_dir)))
553
+
554
+
533
555
  def _clear_cache_on_version_change(cache_root: Path) -> None:
534
556
  """Clear jdtls data cache when the server version changes.
535
557
 
@@ -747,6 +769,7 @@ class JdtlsProxy:
747
769
  if result is None:
748
770
  logger.error("jdtls initialize request failed or timed out")
749
771
  await self.stop()
772
+ await asyncio.get_running_loop().run_in_executor(None, _wipe_data_dir, data_dir)
750
773
  return False
751
774
 
752
775
  self._jdtls_capabilities = result.get("capabilities", {})
@@ -907,8 +930,10 @@ class JdtlsProxy:
907
930
  Maven parent. This covers sibling modules (the common case for
908
931
  cross-module navigation) without ballooning the index.
909
932
 
910
- Falls back to the full IDE workspace root when no intermediate
911
- multi-module parent is found.
933
+ Falls back to the initial module itself when no intermediate
934
+ multi-module parent is found (e.g. flat modules sitting directly
935
+ under the workspace root), keeping the jdtls scope as tight as
936
+ possible.
912
937
 
913
938
  Removes all individually-added module folders to avoid double-indexing
914
939
  with the newly added group root.
@@ -851,6 +851,7 @@ class TestStartPassesEnvToSubprocess:
851
851
  patch.object(JdtlsProxy, "_stderr_reader", side_effect=fake_stderr_reader),
852
852
  patch.object(JdtlsProxy, "send_request", side_effect=fake_send_request),
853
853
  patch.object(JdtlsProxy, "stop", side_effect=lambda: None),
854
+ patch.object(proxy_mod, "_wipe_data_dir"), # avoid blocking rmtree in tests
854
855
  ):
855
856
  ok = await proxy.start({"rootUri": f"file://{tmp_path}"})
856
857
 
@@ -859,6 +860,67 @@ class TestStartPassesEnvToSubprocess:
859
860
  # The crucial assertion: env= was passed through to the subprocess call.
860
861
  assert captured["env"] == sentinel_env
861
862
 
863
+ @pytest.mark.asyncio
864
+ async def test_wipe_runs_in_executor_on_init_timeout(self, tmp_path: Any) -> None:
865
+ """_wipe_data_dir must be dispatched via run_in_executor to avoid blocking the event loop."""
866
+ from unittest.mock import patch
867
+
868
+ import java_functional_lsp.proxy as proxy_mod
869
+
870
+ proxy = JdtlsProxy()
871
+ proxy._jdtls_on_path = True
872
+
873
+ fake_process = MagicMock()
874
+ fake_process.pid = 99999
875
+ fake_process.stdout = MagicMock()
876
+ fake_process.stderr = MagicMock()
877
+ fake_process.returncode = 0
878
+
879
+ async def fake_wait() -> int:
880
+ return 0
881
+
882
+ fake_process.wait = fake_wait
883
+
884
+ async def fake_create_subprocess(*_a: Any, **_kw: Any) -> Any:
885
+ return fake_process
886
+
887
+ async def fake_reader_loop(_: Any) -> None:
888
+ return None
889
+
890
+ async def fake_stderr_reader(_: Any) -> None:
891
+ return None
892
+
893
+ async def fake_send_request(*_a: Any, **_kw: Any) -> None:
894
+ return None # simulate initialize timeout
895
+
896
+ wipe_calls: list[Any] = []
897
+
898
+ async def fake_run_in_executor(_executor: Any, fn: Any, *args: Any) -> Any:
899
+ if fn is proxy_mod._wipe_data_dir:
900
+ wipe_calls.append((fn, args))
901
+ return None
902
+ # Other blocking calls (e.g. _blocking_startup) must run for real
903
+ return fn(*args)
904
+
905
+ fake_loop = MagicMock()
906
+ fake_loop.run_in_executor = fake_run_in_executor
907
+
908
+ with (
909
+ patch.object(proxy_mod.shutil, "which", return_value="/fake/jdtls"),
910
+ patch.object(proxy_mod, "build_jdtls_env", return_value={}),
911
+ patch.object(proxy_mod.asyncio, "create_subprocess_exec", side_effect=fake_create_subprocess),
912
+ patch.object(JdtlsProxy, "_reader_loop", side_effect=fake_reader_loop),
913
+ patch.object(JdtlsProxy, "_stderr_reader", side_effect=fake_stderr_reader),
914
+ patch.object(JdtlsProxy, "send_request", side_effect=fake_send_request),
915
+ patch.object(JdtlsProxy, "stop", side_effect=lambda: None),
916
+ patch("java_functional_lsp.proxy.asyncio.get_running_loop", return_value=fake_loop),
917
+ ):
918
+ ok = await proxy.start({"rootUri": f"file://{tmp_path}"})
919
+
920
+ assert ok is False
921
+ assert len(wipe_calls) == 1
922
+ assert wipe_calls[0][0] is proxy_mod._wipe_data_dir
923
+
862
924
 
863
925
  class TestFindModuleRoot:
864
926
  """Tests for find_module_root — build-file detection for module scoping."""
@@ -1069,17 +1131,24 @@ class TestLazyStart:
1069
1131
  assert event["added"][0]["uri"] == group.as_uri()
1070
1132
  assert event["added"][0]["uri"] != workspace_uri
1071
1133
 
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."""
1134
+ async def test_expand_noop_for_flat_module_directly_under_workspace(self, tmp_path: Path) -> None:
1135
+ """No notification sent for flat modules sitting directly under workspace_root.
1136
+
1137
+ When there is no intermediate group pom between the module and workspace_root,
1138
+ _find_maven_group_root returns the module itself (not workspace_root). Since the
1139
+ module was already added, expand_full_workspace() is a no-op — which is correct:
1140
+ the scope is already as tight as possible and workspace_root's pom.xml
1141
+ (potentially 200+ modules → OOM) is never inspected.
1142
+ """
1074
1143
  from unittest.mock import AsyncMock
1075
1144
 
1076
1145
  from java_functional_lsp.proxy import JdtlsProxy
1077
1146
 
1078
- # Flat layout: workspace/module — no intermediate pom.xml
1147
+ # Flat layout: workspace/module — no intermediate pom.xml between module and workspace
1079
1148
  workspace = tmp_path / "workspace"
1080
1149
  module = workspace / "module"
1081
1150
  module.mkdir(parents=True)
1082
- # workspace/pom.xml has <modules>, but there's nothing in between
1151
+ # workspace/pom.xml has <modules>, but the walk stops BEFORE reaching it
1083
1152
  (workspace / "pom.xml").write_text("<project><modules><module>module</module></modules></project>")
1084
1153
 
1085
1154
  module_uri = module.as_uri()
@@ -1093,9 +1162,9 @@ class TestLazyStart:
1093
1162
  proxy.send_notification = AsyncMock() # type: ignore[assignment]
1094
1163
  await proxy.expand_full_workspace()
1095
1164
 
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
1165
+ # No notification — module was already added and is already the tightest scope
1166
+ proxy.send_notification.assert_not_called() # type: ignore[attr-defined]
1167
+ assert proxy._workspace_expanded is True
1099
1168
 
1100
1169
  async def test_expand_full_workspace_sends_notification(self) -> None:
1101
1170
  from unittest.mock import AsyncMock
@@ -1110,28 +1179,43 @@ class TestLazyStart:
1110
1179
  proxy.send_notification.assert_called_once() # type: ignore[attr-defined]
1111
1180
  assert proxy._workspace_expanded is True
1112
1181
 
1113
- async def test_expand_full_workspace_removes_initial_module(self) -> None:
1114
- """All modules in _states (not just _initial_module_uri) are removed on expansion."""
1182
+ async def test_expand_full_workspace_removes_initial_module(self, tmp_path: Path) -> None:
1183
+ """All modules in _states are removed when expanding to an intermediate group root."""
1115
1184
  from unittest.mock import AsyncMock
1116
1185
 
1117
1186
  from java_functional_lsp.proxy import JdtlsProxy
1118
1187
 
1188
+ # Layout: monorepo/group/module-a, monorepo/group/module-b
1189
+ # group/pom.xml has <modules> → group becomes the expansion target
1190
+ monorepo = tmp_path / "monorepo"
1191
+ group = monorepo / "group"
1192
+ module_a = group / "module-a"
1193
+ module_b = group / "module-b"
1194
+ for d in (module_a, module_b):
1195
+ d.mkdir(parents=True)
1196
+ (group / "pom.xml").write_text("<project><modules><module>module-a</module></modules></project>")
1197
+
1198
+ module_a_uri = module_a.as_uri()
1199
+ module_b_uri = module_b.as_uri()
1200
+ monorepo_uri = monorepo.as_uri()
1201
+ group_uri = group.as_uri()
1202
+
1119
1203
  proxy = JdtlsProxy()
1120
1204
  proxy._available = True
1121
- proxy._original_root_uri = "file:///workspace/monorepo"
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")
1205
+ proxy._original_root_uri = monorepo_uri
1206
+ proxy._initial_module_uri = module_a_uri
1207
+ # Simulate module-a added during start() and module-b via add_module_if_new
1208
+ proxy.modules.mark_added(module_a_uri)
1209
+ proxy.modules.mark_added(module_b_uri)
1126
1210
  proxy.send_notification = AsyncMock() # type: ignore[assignment]
1127
1211
  await proxy.expand_full_workspace()
1128
1212
  proxy.send_notification.assert_called_once() # type: ignore[attr-defined]
1129
1213
  call_args = proxy.send_notification.call_args[0] # type: ignore[attr-defined]
1130
1214
  event = call_args[1]["event"]
1131
1215
  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
1134
- assert event["added"][0]["uri"] == "file:///workspace/monorepo"
1216
+ assert module_a_uri in removed_uris
1217
+ assert module_b_uri in removed_uris
1218
+ assert event["added"][0]["uri"] == group_uri
1135
1219
 
1136
1220
  async def test_expand_full_workspace_noop_when_not_available(self) -> None:
1137
1221
  from unittest.mock import AsyncMock
@@ -1167,7 +1251,8 @@ class TestLazyStart:
1167
1251
  proxy._jdtls_on_path = True
1168
1252
  captured: dict[str, Any] = {}
1169
1253
 
1170
- async def capturing_start(params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1254
+ async def capturing_start(_params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1255
+ _ = config
1171
1256
  captured["module_root_uri"] = module_root_uri
1172
1257
  return False
1173
1258
 
@@ -1191,7 +1276,8 @@ class TestLazyStart:
1191
1276
 
1192
1277
  captured: dict[str, Any] = {}
1193
1278
 
1194
- async def capturing_start(params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1279
+ async def capturing_start(_params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1280
+ _ = config
1195
1281
  captured["module_root_uri"] = module_root_uri
1196
1282
  return False
1197
1283
 
@@ -1215,3 +1301,58 @@ class TestLazyStart:
1215
1301
  module_root = "file:///workspace/monorepo/module-a"
1216
1302
  module_hash = hashlib.sha256(module_root.encode()).hexdigest()[:12]
1217
1303
  assert expected_hash != module_hash
1304
+
1305
+
1306
+ # ---------------------------------------------------------------------------
1307
+ # _wipe_data_dir
1308
+ # ---------------------------------------------------------------------------
1309
+
1310
+
1311
+ class TestWipeDataDir:
1312
+ def test_wipe_removes_directory(self, tmp_path: Path) -> None:
1313
+ """Directory is removed and info is logged."""
1314
+ from java_functional_lsp.proxy import _wipe_data_dir
1315
+
1316
+ data_dir = tmp_path / "data"
1317
+ data_dir.mkdir()
1318
+ (data_dir / "some_file.dat").write_text("x")
1319
+
1320
+ _wipe_data_dir(data_dir)
1321
+
1322
+ assert not data_dir.exists()
1323
+
1324
+ def test_wipe_logs_warning_when_removal_fails(self, tmp_path: Path) -> None:
1325
+ """Warning is logged when rmtree silently fails (dir still present)."""
1326
+ from unittest.mock import patch
1327
+
1328
+ from java_functional_lsp.proxy import _wipe_data_dir
1329
+
1330
+ data_dir = tmp_path / "data"
1331
+ data_dir.mkdir()
1332
+
1333
+ with patch("shutil.rmtree"): # no-op — dir remains
1334
+ with patch("java_functional_lsp.proxy.logger") as mock_logger:
1335
+ _wipe_data_dir(data_dir)
1336
+ mock_logger.warning.assert_called_once()
1337
+ # info must NOT be called when wipe failed
1338
+ mock_logger.info.assert_not_called()
1339
+
1340
+ def test_wipe_logs_info_on_success(self, tmp_path: Path) -> None:
1341
+ """Info is logged (not warning) when directory is successfully removed."""
1342
+ from unittest.mock import patch
1343
+
1344
+ from java_functional_lsp.proxy import _wipe_data_dir
1345
+
1346
+ data_dir = tmp_path / "data"
1347
+ data_dir.mkdir()
1348
+
1349
+ with patch("java_functional_lsp.proxy.logger") as mock_logger:
1350
+ _wipe_data_dir(data_dir)
1351
+ mock_logger.info.assert_called_once()
1352
+ mock_logger.warning.assert_not_called()
1353
+
1354
+ def test_wipe_nonexistent_dir_does_not_raise(self, tmp_path: Path) -> None:
1355
+ """Calling wipe on a non-existent dir is safe."""
1356
+ from java_functional_lsp.proxy import _wipe_data_dir
1357
+
1358
+ _wipe_data_dir(tmp_path / "ghost") # should not raise