java-functional-lsp 0.7.2__tar.gz → 0.7.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 (66) hide show
  1. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/PKG-INFO +1 -1
  2. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/pyproject.toml +1 -1
  3. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/__init__.py +1 -1
  4. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/server.py +89 -16
  5. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_server.py +136 -1
  6. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/uv.lock +1 -1
  7. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.claude-plugin/plugin.json +0 -0
  8. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.githooks/pre-commit +0 -0
  9. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.githooks/pre-push +0 -0
  10. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/CODEOWNERS +0 -0
  11. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  12. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  13. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/SECURITY.md +0 -0
  15. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/dependabot.yml +0 -0
  16. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/release-drafter.yml +0 -0
  17. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/workflows/publish.yml +0 -0
  18. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/workflows/release-drafter.yml +0 -0
  19. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/workflows/stale.yml +0 -0
  20. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/workflows/test.yml +0 -0
  21. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/workflows/update-homebrew.yml +0 -0
  22. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.github/workflows/vscode-ext.yml +0 -0
  23. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/.gitignore +0 -0
  24. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/CONTRIBUTING.md +0 -0
  25. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/LICENSE +0 -0
  26. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/README.md +0 -0
  27. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/SKILL.md +0 -0
  28. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/commands/lint-java.md +0 -0
  29. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/intellij/README.md +0 -0
  30. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/intellij/lsp4ij-template.json +0 -0
  31. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/vscode/.vscodeignore +0 -0
  32. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/vscode/README.md +0 -0
  33. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/vscode/package-lock.json +0 -0
  34. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/vscode/package.json +0 -0
  35. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/vscode/src/extension.ts +0 -0
  36. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/editors/vscode/tsconfig.json +0 -0
  37. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/hooks/hooks.json +0 -0
  38. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/hooks/java_linter_reminder.py +0 -0
  39. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/scripts/ensure-lsp.sh +0 -0
  40. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/scripts/generate-formula.py +0 -0
  41. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/__main__.py +0 -0
  42. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  43. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/base.py +0 -0
  44. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  45. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  46. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  47. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  48. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  49. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/cli.py +0 -0
  50. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/fixes.py +0 -0
  51. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/src/java_functional_lsp/proxy.py +0 -0
  52. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/__init__.py +0 -0
  53. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/conftest.py +0 -0
  54. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_base.py +0 -0
  55. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_cli.py +0 -0
  56. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_config.py +0 -0
  57. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_e2e.py +0 -0
  58. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_e2e_jdtls.py +0 -0
  59. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_exception_checker.py +0 -0
  60. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_fixes.py +0 -0
  61. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_functional_checker.py +0 -0
  62. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_mutation_checker.py +0 -0
  63. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_null_checker.py +0 -0
  64. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_proxy.py +0 -0
  65. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_spring_checker.py +0 -0
  66. {java_functional_lsp-0.7.2 → java_functional_lsp-0.7.3}/tests/test_suppress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.7.2
3
+ Version: 0.7.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.7.2"
7
+ version = "0.7.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.7.2"
3
+ __version__ = "0.7.3"
@@ -228,11 +228,11 @@ def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
228
228
  change=lsp.TextDocumentSyncKind.Full,
229
229
  save=lsp.SaveOptions(include_text=True),
230
230
  ),
231
- completion_provider=lsp.CompletionOptions(trigger_characters=["."]),
232
- hover_provider=True,
233
- definition_provider=True,
234
- references_provider=True,
235
- document_symbol_provider=True,
231
+ # Only advertise capabilities we own (custom diagnostics + code actions).
232
+ # jdtls-dependent features (hover, definition, references, completion,
233
+ # documentSymbol) are registered dynamically after jdtls starts — see
234
+ # on_initialized(). This prevents us from claiming hover when jdtls
235
+ # isn't ready, which would suppress the IDE's diagnostic tooltips.
236
236
  code_action_provider=lsp.CodeActionOptions(
237
237
  code_action_kinds=[lsp.CodeActionKind.QuickFix],
238
238
  ),
@@ -250,10 +250,71 @@ async def on_initialized(params: lsp.InitializedParams) -> None:
250
250
  started = await server._proxy.start(server._init_params)
251
251
  if started:
252
252
  logger.info("jdtls proxy active — full Java language support enabled")
253
+ await _register_jdtls_capabilities()
253
254
  else:
254
255
  logger.info("jdtls proxy unavailable — running with custom rules only")
255
256
 
256
257
 
258
+ _JAVA_SELECTOR = [lsp.TextDocumentFilterLanguage(language="java")]
259
+
260
+ _JDTLS_REG_PREFIX = "jdtls-"
261
+
262
+ # jdtls-dependent capabilities registered dynamically after the proxy starts.
263
+ # Each entry: (id_suffix, LSP method, registration options class, extra kwargs).
264
+ _JDTLS_CAPABILITIES: list[tuple[str, str, type[Any], dict[str, Any]]] = [
265
+ ("completion", lsp.TEXT_DOCUMENT_COMPLETION, lsp.CompletionRegistrationOptions, {"trigger_characters": ["."]}),
266
+ ("hover", lsp.TEXT_DOCUMENT_HOVER, lsp.HoverRegistrationOptions, {}),
267
+ ("definition", lsp.TEXT_DOCUMENT_DEFINITION, lsp.DefinitionRegistrationOptions, {}),
268
+ ("references", lsp.TEXT_DOCUMENT_REFERENCES, lsp.ReferenceRegistrationOptions, {}),
269
+ ("document-symbol", lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL, lsp.DocumentSymbolRegistrationOptions, {}),
270
+ ]
271
+
272
+ # Maps LSP method → handler function for dynamic registration.
273
+ _JDTLS_HANDLERS: dict[str, Any] = {}
274
+
275
+ # Set after first successful registration to prevent FeatureAlreadyRegisteredError.
276
+ _jdtls_capabilities_registered = False
277
+
278
+
279
+ def _build_jdtls_registrations() -> list[lsp.Registration]:
280
+ """Build LSP Registration objects for jdtls-dependent capabilities."""
281
+ return [
282
+ lsp.Registration(
283
+ id=f"{_JDTLS_REG_PREFIX}{suffix}",
284
+ method=method,
285
+ register_options=_converter.unstructure(opts_cls(document_selector=_JAVA_SELECTOR, **extra)),
286
+ )
287
+ for suffix, method, opts_cls, extra in _JDTLS_CAPABILITIES
288
+ ]
289
+
290
+
291
+ async def _register_jdtls_capabilities() -> None:
292
+ """Dynamically register jdtls-dependent capabilities after the proxy starts.
293
+
294
+ We don't advertise these in the static InitializeResult because doing so
295
+ would make the IDE defer hover/definition/etc to us even before jdtls is
296
+ ready, which suppresses the IDE's built-in diagnostic tooltips.
297
+
298
+ Idempotent: safe to call multiple times (e.g., proxy restart).
299
+ """
300
+ global _jdtls_capabilities_registered
301
+ if _jdtls_capabilities_registered:
302
+ return
303
+
304
+ try:
305
+ # Register handlers so pygls dispatches incoming requests to them.
306
+ for method, handler in _JDTLS_HANDLERS.items():
307
+ server.feature(method)(handler)
308
+
309
+ # Tell the client we now support these capabilities.
310
+ registrations = _build_jdtls_registrations()
311
+ await server.client_register_capability_async(lsp.RegistrationParams(registrations=registrations))
312
+ _jdtls_capabilities_registered = True
313
+ logger.info("Dynamically registered jdtls capabilities (hover, definition, references, completion, symbol)")
314
+ except Exception:
315
+ logger.warning("Failed to dynamically register jdtls capabilities", exc_info=True)
316
+
317
+
257
318
  # --- Document sync (forward to jdtls + run custom analyzers) ---
258
319
 
259
320
 
@@ -311,11 +372,15 @@ async def on_did_close(params: lsp.DidCloseTextDocumentParams) -> None:
311
372
  await server._proxy.send_notification("textDocument/didClose", _serialize_params(params))
312
373
 
313
374
 
314
- # --- Forwarded features (jdtls passthrough) ---
375
+ # --- jdtls passthrough handlers (registered dynamically, NOT at module level) ---
376
+ #
377
+ # These are NOT decorated with @server.feature because pygls auto-advertises
378
+ # capabilities for decorated handlers. Instead, they are collected in
379
+ # _JDTLS_HANDLERS and registered inside _register_jdtls_capabilities() so
380
+ # they only activate after jdtls starts.
315
381
 
316
382
 
317
- @server.feature(lsp.TEXT_DOCUMENT_COMPLETION)
318
- async def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | None:
383
+ async def _on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | None:
319
384
  """Forward completion request to jdtls."""
320
385
  if not server._proxy.is_available:
321
386
  return None
@@ -328,8 +393,7 @@ async def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | No
328
393
  return None
329
394
 
330
395
 
331
- @server.feature(lsp.TEXT_DOCUMENT_HOVER)
332
- async def on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
396
+ async def _on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
333
397
  """Forward hover request to jdtls."""
334
398
  if not server._proxy.is_available:
335
399
  return None
@@ -342,8 +406,7 @@ async def on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
342
406
  return None
343
407
 
344
408
 
345
- @server.feature(lsp.TEXT_DOCUMENT_DEFINITION)
346
- async def on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | None:
409
+ async def _on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | None:
347
410
  """Forward go-to-definition request to jdtls."""
348
411
  if not server._proxy.is_available:
349
412
  return None
@@ -358,8 +421,7 @@ async def on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | No
358
421
  return None
359
422
 
360
423
 
361
- @server.feature(lsp.TEXT_DOCUMENT_REFERENCES)
362
- async def on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | None:
424
+ async def _on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | None:
363
425
  """Forward find-references request to jdtls."""
364
426
  if not server._proxy.is_available:
365
427
  return None
@@ -372,8 +434,7 @@ async def on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | Non
372
434
  return None
373
435
 
374
436
 
375
- @server.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
376
- async def on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.DocumentSymbol] | None:
437
+ async def _on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.DocumentSymbol] | None:
377
438
  """Forward document symbol request to jdtls."""
378
439
  if not server._proxy.is_available:
379
440
  return None
@@ -386,6 +447,18 @@ async def on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.Docum
386
447
  return None
387
448
 
388
449
 
450
+ # Populate handler map for dynamic registration.
451
+ _JDTLS_HANDLERS.update(
452
+ {
453
+ lsp.TEXT_DOCUMENT_COMPLETION: _on_completion,
454
+ lsp.TEXT_DOCUMENT_HOVER: _on_hover,
455
+ lsp.TEXT_DOCUMENT_DEFINITION: _on_definition,
456
+ lsp.TEXT_DOCUMENT_REFERENCES: _on_references,
457
+ lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL: _on_document_symbol,
458
+ }
459
+ )
460
+
461
+
389
462
  # --- Code actions (quick fixes) ---
390
463
 
391
464
  # Human-readable titles for code actions
@@ -290,6 +290,130 @@ class TestServerInternals:
290
290
  finally:
291
291
  server.workspace.remove_text_document(uri)
292
292
 
293
+ def test_init_capabilities_exclude_jdtls_features(self) -> None:
294
+ """Static capabilities must NOT include hover/definition/references/completion/documentSymbol.
295
+
296
+ These are registered dynamically after jdtls starts, so the IDE doesn't
297
+ suppress diagnostic tooltips while jdtls is unavailable.
298
+ """
299
+ from java_functional_lsp.server import on_initialize
300
+
301
+ result = on_initialize(
302
+ lsp.InitializeParams(
303
+ process_id=1,
304
+ root_uri="file:///tmp",
305
+ capabilities=lsp.ClientCapabilities(),
306
+ )
307
+ )
308
+ caps = result.capabilities
309
+ assert caps.code_action_provider is not None
310
+ assert caps.text_document_sync is not None
311
+ assert caps.hover_provider is None
312
+ assert caps.definition_provider is None
313
+ assert caps.references_provider is None
314
+ assert caps.completion_provider is None
315
+ assert caps.document_symbol_provider is None
316
+
317
+ def test_build_jdtls_registrations(self) -> None:
318
+ """_build_jdtls_registrations returns one Registration per jdtls capability, each scoped to java files."""
319
+ from java_functional_lsp.server import _JDTLS_REG_PREFIX, _build_jdtls_registrations
320
+
321
+ regs = _build_jdtls_registrations()
322
+ assert len(regs) == 5
323
+ methods = {r.method for r in regs}
324
+ assert lsp.TEXT_DOCUMENT_HOVER in methods
325
+ assert lsp.TEXT_DOCUMENT_DEFINITION in methods
326
+ assert lsp.TEXT_DOCUMENT_REFERENCES in methods
327
+ assert lsp.TEXT_DOCUMENT_COMPLETION in methods
328
+ assert lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL in methods
329
+ # All IDs are unique and use the shared prefix
330
+ ids = {r.id for r in regs}
331
+ assert len(ids) == 5
332
+ assert all(rid.startswith(_JDTLS_REG_PREFIX) for rid in ids)
333
+ # All have java document selector with correct language
334
+ for r in regs:
335
+ assert r.register_options is not None
336
+ selectors = r.register_options["documentSelector"]
337
+ assert any(s.get("language") == "java" for s in selectors)
338
+ # Completion has triggerCharacters
339
+ comp = next(r for r in regs if r.method == lsp.TEXT_DOCUMENT_COMPLETION)
340
+ assert comp.register_options.get("triggerCharacters") == ["."]
341
+
342
+ async def test_register_jdtls_capabilities_logs_on_failure(self, caplog: Any) -> None:
343
+ """_register_jdtls_capabilities logs a warning when the client rejects."""
344
+ import logging
345
+ from unittest.mock import AsyncMock, MagicMock, patch
346
+
347
+ import java_functional_lsp.server as srv_mod
348
+ from java_functional_lsp.server import server as srv
349
+
350
+ # Patch both server.feature (to avoid FeatureAlreadyRegisteredError on
351
+ # the shared singleton) and client_register_capability_async (to trigger error).
352
+ mock_reg = AsyncMock(side_effect=Exception("no"))
353
+ mock_feature = MagicMock(return_value=lambda fn: fn)
354
+ old_flag = srv_mod._jdtls_capabilities_registered
355
+ srv_mod._jdtls_capabilities_registered = False
356
+ try:
357
+ with (
358
+ caplog.at_level(logging.WARNING, logger="java_functional_lsp.server"),
359
+ patch.object(srv, "client_register_capability_async", mock_reg),
360
+ patch.object(srv, "feature", mock_feature),
361
+ ):
362
+ await srv_mod._register_jdtls_capabilities()
363
+ finally:
364
+ srv_mod._jdtls_capabilities_registered = old_flag
365
+ assert any("Failed to dynamically register" in r.getMessage() for r in caplog.records)
366
+
367
+ async def test_register_jdtls_capabilities_happy_path(self, caplog: Any) -> None:
368
+ """On success, handlers are registered and info log is emitted."""
369
+ import logging
370
+ from unittest.mock import AsyncMock, MagicMock, patch
371
+
372
+ import java_functional_lsp.server as srv_mod
373
+ from java_functional_lsp.server import server as srv
374
+
375
+ mock_reg = AsyncMock(return_value=None)
376
+ registered_methods: list[str] = []
377
+ mock_feature = MagicMock(side_effect=lambda m: registered_methods.append(m) or (lambda fn: fn))
378
+ old_flag = srv_mod._jdtls_capabilities_registered
379
+ srv_mod._jdtls_capabilities_registered = False
380
+ try:
381
+ with (
382
+ caplog.at_level(logging.INFO, logger="java_functional_lsp.server"),
383
+ patch.object(srv, "client_register_capability_async", mock_reg),
384
+ patch.object(srv, "feature", mock_feature),
385
+ ):
386
+ await srv_mod._register_jdtls_capabilities()
387
+ finally:
388
+ srv_mod._jdtls_capabilities_registered = old_flag
389
+ # Handlers were registered for all 5 methods
390
+ assert lsp.TEXT_DOCUMENT_HOVER in registered_methods
391
+ assert lsp.TEXT_DOCUMENT_COMPLETION in registered_methods
392
+ assert lsp.TEXT_DOCUMENT_DEFINITION in registered_methods
393
+ assert lsp.TEXT_DOCUMENT_REFERENCES in registered_methods
394
+ assert lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL in registered_methods
395
+ # client_register_capability_async was called
396
+ mock_reg.assert_called_once()
397
+ # Success log emitted
398
+ assert any("Dynamically registered" in r.getMessage() for r in caplog.records)
399
+
400
+ async def test_register_jdtls_capabilities_idempotent(self) -> None:
401
+ """Second call is a no-op (idempotency guard)."""
402
+ from unittest.mock import AsyncMock, patch
403
+
404
+ import java_functional_lsp.server as srv_mod
405
+ from java_functional_lsp.server import server as srv
406
+
407
+ mock_reg = AsyncMock()
408
+ old_flag = srv_mod._jdtls_capabilities_registered
409
+ srv_mod._jdtls_capabilities_registered = True
410
+ try:
411
+ with patch.object(srv, "client_register_capability_async", mock_reg):
412
+ await srv_mod._register_jdtls_capabilities()
413
+ finally:
414
+ srv_mod._jdtls_capabilities_registered = old_flag
415
+ mock_reg.assert_not_called()
416
+
293
417
  def test_serialize_params_camelcase(self) -> None:
294
418
  from java_functional_lsp.server import _serialize_params
295
419
 
@@ -396,11 +520,22 @@ class TestLspLifecycle:
396
520
  """Full LSP lifecycle tests via real stdio transport — zero mocks."""
397
521
 
398
522
  async def test_initialize_reports_capabilities(self, lsp_client: LanguageClient) -> None:
399
- """Server advertises codeActionProvider and textDocumentSync."""
523
+ """Server advertises codeActionProvider and textDocumentSync but NOT jdtls features.
524
+
525
+ jdtls-dependent capabilities (hover, definition, references, completion,
526
+ documentSymbol) are registered dynamically after jdtls starts, so they
527
+ should NOT appear in the static InitializeResult.
528
+ """
400
529
  caps = lsp_client._server_capabilities # type: ignore[attr-defined]
401
530
  assert caps is not None
402
531
  assert caps.code_action_provider is not None
403
532
  assert caps.text_document_sync is not None
533
+ # jdtls features are NOT statically advertised (registered dynamically)
534
+ assert caps.hover_provider is None
535
+ assert caps.definition_provider is None
536
+ assert caps.references_provider is None
537
+ assert caps.completion_provider is None
538
+ assert caps.document_symbol_provider is None
404
539
 
405
540
  async def test_null_return_diagnostic_published(self, lsp_client: LanguageClient) -> None:
406
541
  """didOpen a file with ``return null`` → server publishes null-return diagnostic."""
@@ -184,7 +184,7 @@ wheels = [
184
184
 
185
185
  [[package]]
186
186
  name = "java-functional-lsp"
187
- version = "0.7.2"
187
+ version = "0.7.3"
188
188
  source = { editable = "." }
189
189
  dependencies = [
190
190
  { name = "pygls" },