java-functional-lsp 0.9.2__tar.gz → 0.9.3__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.3}/PKG-INFO +1 -1
  2. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/pyproject.toml +1 -1
  3. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/__init__.py +1 -1
  4. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/proxy.py +17 -0
  5. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_proxy.py +121 -2
  6. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.claude-plugin/plugin.json +0 -0
  7. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.githooks/pre-commit +0 -0
  8. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.githooks/pre-push +0 -0
  9. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/CODEOWNERS +0 -0
  10. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  11. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  12. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/SECURITY.md +0 -0
  14. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/dependabot.yml +0 -0
  15. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/release-drafter.yml +0 -0
  16. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/workflows/publish.yml +0 -0
  17. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/workflows/release-drafter.yml +0 -0
  18. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/workflows/stale.yml +0 -0
  19. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/workflows/test.yml +0 -0
  20. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/workflows/update-homebrew.yml +0 -0
  21. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.github/workflows/vscode-ext.yml +0 -0
  22. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/.gitignore +0 -0
  23. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/CONTRIBUTING.md +0 -0
  24. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/LICENSE +0 -0
  25. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/README.md +0 -0
  26. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/SKILL.md +0 -0
  27. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/commands/lint-java.md +0 -0
  28. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/intellij/README.md +0 -0
  29. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/intellij/lsp4ij-template.json +0 -0
  30. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/vscode/.vscodeignore +0 -0
  31. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/vscode/README.md +0 -0
  32. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/vscode/package-lock.json +0 -0
  33. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/vscode/package.json +0 -0
  34. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/vscode/src/extension.ts +0 -0
  35. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/editors/vscode/tsconfig.json +0 -0
  36. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/hooks/hooks.json +0 -0
  37. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/hooks/java_linter_reminder.py +0 -0
  38. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/scripts/ensure-lsp.sh +0 -0
  39. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/scripts/generate-formula.py +0 -0
  40. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/__main__.py +0 -0
  41. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  42. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/analyzers/base.py +0 -0
  43. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  44. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  45. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  46. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  47. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  48. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/cli.py +0 -0
  49. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/fixes.py +0 -0
  50. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/merkle.py +0 -0
  51. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/src/java_functional_lsp/server.py +0 -0
  52. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/__init__.py +0 -0
  53. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/conftest.py +0 -0
  54. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_base.py +0 -0
  55. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_cli.py +0 -0
  56. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_config.py +0 -0
  57. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_e2e.py +0 -0
  58. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_e2e_jdtls.py +0 -0
  59. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_exception_checker.py +0 -0
  60. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_fixes.py +0 -0
  61. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_functional_checker.py +0 -0
  62. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_merkle.py +0 -0
  63. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_merkle_proxy.py +0 -0
  64. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_mutation_checker.py +0 -0
  65. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_null_checker.py +0 -0
  66. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_server.py +0 -0
  67. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_spring_checker.py +0 -0
  68. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/tests/test_suppress.py +0 -0
  69. {java_functional_lsp-0.9.2 → java_functional_lsp-0.9.3}/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.3
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.3"
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.3"
@@ -530,6 +530,22 @@ def _compute_module_diff(
530
530
  return (diff, current)
531
531
 
532
532
 
533
+ def _wipe_data_dir(data_dir: Path) -> None:
534
+ """Remove *data_dir* so the next jdtls startup gets a clean cold start.
535
+
536
+ Called after an initialize timeout to recover from a corrupted OSGi
537
+ data-dir (e.g. left behind after an OOM crash).
538
+ """
539
+ shutil.rmtree(data_dir, ignore_errors=True)
540
+ if data_dir.exists():
541
+ logger.warning(
542
+ "jdtls: failed to wipe data-dir %s — next retry may hang again",
543
+ _redact_path(str(data_dir)),
544
+ )
545
+ else:
546
+ logger.info("jdtls: wiped data-dir %s after init timeout", _redact_path(str(data_dir)))
547
+
548
+
533
549
  def _clear_cache_on_version_change(cache_root: Path) -> None:
534
550
  """Clear jdtls data cache when the server version changes.
535
551
 
@@ -747,6 +763,7 @@ class JdtlsProxy:
747
763
  if result is None:
748
764
  logger.error("jdtls initialize request failed or timed out")
749
765
  await self.stop()
766
+ await asyncio.get_running_loop().run_in_executor(None, _wipe_data_dir, data_dir)
750
767
  return False
751
768
 
752
769
  self._jdtls_capabilities = result.get("capabilities", {})
@@ -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."""
@@ -1167,7 +1229,8 @@ class TestLazyStart:
1167
1229
  proxy._jdtls_on_path = True
1168
1230
  captured: dict[str, Any] = {}
1169
1231
 
1170
- async def capturing_start(params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1232
+ async def capturing_start(_params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1233
+ _ = config
1171
1234
  captured["module_root_uri"] = module_root_uri
1172
1235
  return False
1173
1236
 
@@ -1191,7 +1254,8 @@ class TestLazyStart:
1191
1254
 
1192
1255
  captured: dict[str, Any] = {}
1193
1256
 
1194
- async def capturing_start(params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1257
+ async def capturing_start(_params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1258
+ _ = config
1195
1259
  captured["module_root_uri"] = module_root_uri
1196
1260
  return False
1197
1261
 
@@ -1215,3 +1279,58 @@ class TestLazyStart:
1215
1279
  module_root = "file:///workspace/monorepo/module-a"
1216
1280
  module_hash = hashlib.sha256(module_root.encode()).hexdigest()[:12]
1217
1281
  assert expected_hash != module_hash
1282
+
1283
+
1284
+ # ---------------------------------------------------------------------------
1285
+ # _wipe_data_dir
1286
+ # ---------------------------------------------------------------------------
1287
+
1288
+
1289
+ class TestWipeDataDir:
1290
+ def test_wipe_removes_directory(self, tmp_path: Path) -> None:
1291
+ """Directory is removed and info is logged."""
1292
+ from java_functional_lsp.proxy import _wipe_data_dir
1293
+
1294
+ data_dir = tmp_path / "data"
1295
+ data_dir.mkdir()
1296
+ (data_dir / "some_file.dat").write_text("x")
1297
+
1298
+ _wipe_data_dir(data_dir)
1299
+
1300
+ assert not data_dir.exists()
1301
+
1302
+ def test_wipe_logs_warning_when_removal_fails(self, tmp_path: Path) -> None:
1303
+ """Warning is logged when rmtree silently fails (dir still present)."""
1304
+ from unittest.mock import patch
1305
+
1306
+ from java_functional_lsp.proxy import _wipe_data_dir
1307
+
1308
+ data_dir = tmp_path / "data"
1309
+ data_dir.mkdir()
1310
+
1311
+ with patch("shutil.rmtree"): # no-op — dir remains
1312
+ with patch("java_functional_lsp.proxy.logger") as mock_logger:
1313
+ _wipe_data_dir(data_dir)
1314
+ mock_logger.warning.assert_called_once()
1315
+ # info must NOT be called when wipe failed
1316
+ mock_logger.info.assert_not_called()
1317
+
1318
+ def test_wipe_logs_info_on_success(self, tmp_path: Path) -> None:
1319
+ """Info is logged (not warning) when directory is successfully removed."""
1320
+ from unittest.mock import patch
1321
+
1322
+ from java_functional_lsp.proxy import _wipe_data_dir
1323
+
1324
+ data_dir = tmp_path / "data"
1325
+ data_dir.mkdir()
1326
+
1327
+ with patch("java_functional_lsp.proxy.logger") as mock_logger:
1328
+ _wipe_data_dir(data_dir)
1329
+ mock_logger.info.assert_called_once()
1330
+ mock_logger.warning.assert_not_called()
1331
+
1332
+ def test_wipe_nonexistent_dir_does_not_raise(self, tmp_path: Path) -> None:
1333
+ """Calling wipe on a non-existent dir is safe."""
1334
+ from java_functional_lsp.proxy import _wipe_data_dir
1335
+
1336
+ _wipe_data_dir(tmp_path / "ghost") # should not raise