java-functional-lsp 0.8.0__tar.gz → 0.8.1__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.8.0 → java_functional_lsp-0.8.1}/PKG-INFO +3 -1
  2. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/README.md +2 -0
  3. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/intellij/README.md +2 -1
  4. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/pyproject.toml +1 -1
  5. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/__init__.py +1 -1
  6. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/server.py +71 -3
  7. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_server.py +94 -0
  8. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/uv.lock +4 -4
  9. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.claude-plugin/plugin.json +0 -0
  10. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.githooks/pre-commit +0 -0
  11. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.githooks/pre-push +0 -0
  12. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/CODEOWNERS +0 -0
  13. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  14. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  15. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/SECURITY.md +0 -0
  17. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/dependabot.yml +0 -0
  18. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/release-drafter.yml +0 -0
  19. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/workflows/publish.yml +0 -0
  20. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/workflows/release-drafter.yml +0 -0
  21. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/workflows/stale.yml +0 -0
  22. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/workflows/test.yml +0 -0
  23. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/workflows/update-homebrew.yml +0 -0
  24. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.github/workflows/vscode-ext.yml +0 -0
  25. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/.gitignore +0 -0
  26. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/CONTRIBUTING.md +0 -0
  27. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/LICENSE +0 -0
  28. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/SKILL.md +0 -0
  29. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/commands/lint-java.md +0 -0
  30. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/intellij/lsp4ij-template.json +0 -0
  31. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/vscode/.vscodeignore +0 -0
  32. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/vscode/README.md +0 -0
  33. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/vscode/package-lock.json +0 -0
  34. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/vscode/package.json +0 -0
  35. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/vscode/src/extension.ts +0 -0
  36. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/editors/vscode/tsconfig.json +0 -0
  37. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/hooks/hooks.json +0 -0
  38. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/hooks/java_linter_reminder.py +0 -0
  39. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/scripts/ensure-lsp.sh +0 -0
  40. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/scripts/generate-formula.py +0 -0
  41. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/__main__.py +0 -0
  42. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  43. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/base.py +0 -0
  44. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  45. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  46. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  47. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  48. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  49. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/cli.py +0 -0
  50. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/fixes.py +0 -0
  51. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/src/java_functional_lsp/proxy.py +0 -0
  52. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/__init__.py +0 -0
  53. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/conftest.py +0 -0
  54. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_base.py +0 -0
  55. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_cli.py +0 -0
  56. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_config.py +0 -0
  57. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_e2e.py +0 -0
  58. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_e2e_jdtls.py +0 -0
  59. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_exception_checker.py +0 -0
  60. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_fixes.py +0 -0
  61. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_functional_checker.py +0 -0
  62. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_mutation_checker.py +0 -0
  63. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_null_checker.py +0 -0
  64. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_proxy.py +0 -0
  65. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/tests/test_spring_checker.py +0 -0
  66. {java_functional_lsp-0.8.0 → java_functional_lsp-0.8.1}/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.8.0
3
+ Version: 0.8.1
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
@@ -128,6 +128,8 @@ Use the [LSP4IJ](https://github.com/redhat-developer/lsp4ij) plugin (works on Co
128
128
  2. **Settings** → **Languages & Frameworks** → **Language Servers** → **`+`**
129
129
  3. Set **Command**: `java-functional-lsp`, then in **Mappings** → **File name patterns** add `*.java` with Language Id `java`
130
130
 
131
+ The server automatically detects JetBrains IDEs and disables the jdtls proxy (IntelliJ provides native Java support). To force-enable jdtls, set `JAVA_FUNCTIONAL_LSP_JDTLS=on` in the server command environment.
132
+
131
133
  See [editors/intellij/README.md](editors/intellij/README.md) for detailed instructions.
132
134
 
133
135
  ### Claude Code
@@ -100,6 +100,8 @@ Use the [LSP4IJ](https://github.com/redhat-developer/lsp4ij) plugin (works on Co
100
100
  2. **Settings** → **Languages & Frameworks** → **Language Servers** → **`+`**
101
101
  3. Set **Command**: `java-functional-lsp`, then in **Mappings** → **File name patterns** add `*.java` with Language Id `java`
102
102
 
103
+ The server automatically detects JetBrains IDEs and disables the jdtls proxy (IntelliJ provides native Java support). To force-enable jdtls, set `JAVA_FUNCTIONAL_LSP_JDTLS=on` in the server command environment.
104
+
103
105
  See [editors/intellij/README.md](editors/intellij/README.md) for detailed instructions.
104
106
 
105
107
  ### Claude Code
@@ -73,7 +73,7 @@ Project-level rules are configured via `.java-functional-lsp.json` in your proje
73
73
 
74
74
  ## Coexistence with IntelliJ's Java Support
75
75
 
76
- LSP4IJ is designed to **supplement** IntelliJ's native Java support, not replace it. Your custom functional programming diagnostics appear alongside IntelliJ's built-in inspections. No conflicts.
76
+ The server automatically detects JetBrains IDEs and disables the jdtls proxy — IntelliJ provides its own Java language features (completions, hover, go-to-definition, compile errors). Only the 16 custom functional programming rules run, and they appear alongside IntelliJ's built-in inspections.
77
77
 
78
78
  ## Troubleshooting
79
79
 
@@ -85,6 +85,7 @@ LSP4IJ is designed to **supplement** IntelliJ's native Java support, not replace
85
85
  - Ensure the file mapping is set to Language: `Java`, Language ID: `java`
86
86
  - Check that `.java-functional-lsp.json` doesn't have rules set to `"off"`
87
87
  - Try restarting the language server: **Tools** → **Language Servers** → **Restart**
88
+ - If diagnostics worked before but stopped, the jdtls proxy may be interfering. Set `JAVA_FUNCTIONAL_LSP_JDTLS=off` in the server command environment to force-disable jdtls. The server auto-detects JetBrains IDEs and disables jdtls by default (since IntelliJ provides native Java support), but this can be overridden with `JAVA_FUNCTIONAL_LSP_JDTLS=on`.
88
89
 
89
90
  ### PATH not found
90
91
  - IntelliJ may not inherit your shell's PATH. Use the full absolute path to the binary in the server command.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.8.0"
7
+ version = "0.8.1"
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.8.0"
3
+ __version__ = "0.8.1"
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import asyncio
10
10
  import json
11
11
  import logging
12
+ import os
12
13
  import re
13
14
  import sys
14
15
  from pathlib import Path
@@ -46,6 +47,10 @@ _ANALYZERS: list[Analyzer] = [
46
47
  FunctionalChecker(),
47
48
  ]
48
49
 
50
+ # JetBrains IDE detection — all products contain one of these substrings in clientInfo.name.
51
+ # When detected, jdtls is skipped because IntelliJ provides native Java language support.
52
+ _JDTLS_SKIP_CLIENTS = ("IntelliJ", "JetBrains")
53
+
49
54
  #: LSP-aware cattrs converter. Unstructures to the LSP JSON shape
50
55
  #: (camelCase field names, discriminated unions, None-field pruning) and
51
56
  #: correspondingly structures from the same shape. Using a vanilla
@@ -66,6 +71,9 @@ class JavaFunctionalLspServer(LanguageServer):
66
71
  self._init_params: dict[str, Any] = {}
67
72
  self._proxy = JdtlsProxy(on_diagnostics=self._on_jdtls_diagnostics)
68
73
  self._user_suppress_patterns: list[re.Pattern[str]] = []
74
+ self._skip_jdtls: bool = False
75
+ self._skip_jdtls_registration: bool = False
76
+ self._init_generation: int = 0
69
77
 
70
78
  def _on_jdtls_diagnostics(self, uri: str, diagnostics: list[Any]) -> None:
71
79
  """Called when jdtls publishes diagnostics — merge with custom and re-publish.
@@ -199,6 +207,9 @@ def _jdtls_raw_to_lsp_diagnostics(raw_diagnostics: list[Any]) -> list[lsp.Diagno
199
207
  return result
200
208
 
201
209
 
210
+ _MAX_PATTERN_LENGTH = 500 # Cap regex length to mitigate ReDoS from pathological patterns
211
+
212
+
202
213
  def _compile_user_patterns(config: dict[str, Any]) -> list[re.Pattern[str]]:
203
214
  """Compile user-defined suppressJdtlsPatterns from config."""
204
215
  raw = config.get("suppressJdtlsPatterns", [])
@@ -209,6 +220,9 @@ def _compile_user_patterns(config: dict[str, Any]) -> list[re.Pattern[str]]:
209
220
  for entry in raw:
210
221
  if not isinstance(entry, str):
211
222
  continue
223
+ if len(entry) > _MAX_PATTERN_LENGTH:
224
+ logger.warning("suppressJdtlsPatterns entry too long (%d chars, max %d)", len(entry), _MAX_PATTERN_LENGTH)
225
+ continue
212
226
  try:
213
227
  patterns.append(re.compile(entry))
214
228
  except re.error as e:
@@ -276,6 +290,38 @@ def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
276
290
  server._config = _load_config(root)
277
291
  server._user_suppress_patterns = _compile_user_patterns(server._config)
278
292
 
293
+ # Determine jdtls mode: env var takes priority, then auto-detect JetBrains IDEs.
294
+ client_name = (params.client_info.name if params.client_info else "")[:200]
295
+ logger.info("LSP client: %s", client_name or "(unknown)")
296
+
297
+ # Reset flags so re-initialization (non-standard but defensive) starts clean.
298
+ # Bump generation so in-flight _register_jdtls_capabilities from a prior
299
+ # session detects the stale context and aborts.
300
+ global _jdtls_capabilities_registered
301
+ server._skip_jdtls = False
302
+ server._skip_jdtls_registration = False
303
+ server._init_generation += 1
304
+ _jdtls_capabilities_registered = False
305
+
306
+ jdtls_override = os.environ.get("JAVA_FUNCTIONAL_LSP_JDTLS", "").strip().lower()
307
+ if jdtls_override == "off":
308
+ server._skip_jdtls = True
309
+ logger.info("jdtls proxy disabled via JAVA_FUNCTIONAL_LSP_JDTLS=off")
310
+ elif jdtls_override == "on":
311
+ logger.info("jdtls proxy force-enabled via JAVA_FUNCTIONAL_LSP_JDTLS=on")
312
+ elif jdtls_override == "no-register":
313
+ server._skip_jdtls_registration = True
314
+ logger.info("jdtls dynamic registration disabled via JAVA_FUNCTIONAL_LSP_JDTLS=no-register")
315
+ elif jdtls_override:
316
+ logger.warning("Unknown JAVA_FUNCTIONAL_LSP_JDTLS value %r; expected off/on/no-register", jdtls_override)
317
+ elif any(token in client_name for token in _JDTLS_SKIP_CLIENTS):
318
+ server._skip_jdtls = True
319
+ logger.info(
320
+ "JetBrains IDE detected (%s) — skipping jdtls proxy "
321
+ "(IDE provides native Java support; override with JAVA_FUNCTIONAL_LSP_JDTLS=on)",
322
+ client_name,
323
+ )
324
+
279
325
  return lsp.InitializeResult(
280
326
  capabilities=lsp.ServerCapabilities(
281
327
  text_document_sync=lsp.TextDocumentSyncOptions(
@@ -302,6 +348,9 @@ async def on_initialized(params: lsp.InitializedParams) -> None:
302
348
  "java-functional-lsp initialized (rules: %s)",
303
349
  list(server._config.get("rules", {}).keys()) or "all defaults",
304
350
  )
351
+ if server._skip_jdtls:
352
+ logger.info("jdtls proxy disabled — custom rules only")
353
+ return
305
354
  if server._proxy.check_available():
306
355
  logger.info("jdtls found on PATH — will start lazily on first file open")
307
356
  else:
@@ -349,11 +398,15 @@ async def _register_jdtls_capabilities() -> None:
349
398
  ready, which suppresses the IDE's built-in diagnostic tooltips.
350
399
 
351
400
  Idempotent: safe to call multiple times (e.g., proxy restart).
401
+ Uses a generation counter to detect stale registrations from a prior
402
+ initialize cycle (defensive against non-standard re-initialization).
352
403
  """
353
404
  global _jdtls_capabilities_registered
354
405
  if _jdtls_capabilities_registered:
355
406
  return
356
407
 
408
+ generation = server._init_generation
409
+
357
410
  try:
358
411
  # Register handlers so pygls dispatches incoming requests to them.
359
412
  for method, handler in _JDTLS_HANDLERS.items():
@@ -362,6 +415,12 @@ async def _register_jdtls_capabilities() -> None:
362
415
  # Tell the client we now support these capabilities.
363
416
  registrations = _build_jdtls_registrations()
364
417
  await server.client_register_capability_async(lsp.RegistrationParams(registrations=registrations))
418
+
419
+ # Bail if a re-initialize happened while we were awaiting.
420
+ if generation != server._init_generation:
421
+ logger.info("Discarding stale jdtls capability registration (re-initialize detected)")
422
+ return
423
+
365
424
  _jdtls_capabilities_registered = True
366
425
  logger.info("Dynamically registered jdtls capabilities (hover, definition, references, completion, symbol)")
367
426
  except Exception:
@@ -389,6 +448,8 @@ async def _deferred_validate(uri: str) -> None:
389
448
 
390
449
  def _forward_or_queue(method: str, serialized: Any) -> None:
391
450
  """Forward a notification to jdtls if available, or queue it if starting."""
451
+ if server._skip_jdtls:
452
+ return
392
453
  if server._proxy.is_available:
393
454
  task = asyncio.create_task(server._proxy.send_notification(method, serialized))
394
455
  _bg_tasks.add(task)
@@ -406,14 +467,18 @@ async def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None:
406
467
  didOpen response isn't delayed by jdtls cold-start.
407
468
  """
408
469
  uri = params.text_document.uri
409
- serialized = _serialize_params(params)
410
470
 
411
- if server._proxy.is_available:
471
+ if server._skip_jdtls:
472
+ # Skip all jdtls forwarding — custom diagnostics only.
473
+ pass
474
+ elif server._proxy.is_available:
412
475
  # Fast path: jdtls running. Forward didOpen + add module if new.
476
+ serialized = _serialize_params(params)
413
477
  await server._proxy.send_notification("textDocument/didOpen", serialized)
414
478
  await server._proxy.add_module_if_new(uri)
415
479
  elif server._proxy._jdtls_on_path and not server._proxy._start_failed:
416
480
  # Queue the didOpen (whether this is the first file or a subsequent one during startup).
481
+ serialized = _serialize_params(params)
417
482
  server._proxy.queue_notification("textDocument/didOpen", serialized)
418
483
  if not server._proxy._lazy_start_fired:
419
484
  # First file: kick off lazy start in background.
@@ -474,7 +539,10 @@ async def _lazy_start_jdtls(file_uri: str) -> None:
474
539
  started = await server._proxy.ensure_started(server._init_params, file_uri, config=server._config)
475
540
  if started:
476
541
  logger.info("jdtls proxy active — full Java language support enabled")
477
- await _register_jdtls_capabilities()
542
+ if server._skip_jdtls_registration:
543
+ logger.info("Skipping dynamic capability registration (no-register mode)")
544
+ else:
545
+ await _register_jdtls_capabilities()
478
546
  await server._proxy.flush_queued_notifications()
479
547
  except Exception:
480
548
  logger.warning("jdtls lazy start failed", exc_info=True)
@@ -750,6 +750,100 @@ class TestJdtlsIsolation:
750
750
  # If we get here without exception, the handler caught it correctly
751
751
 
752
752
 
753
+ class TestJdtlsSkipDetection:
754
+ """Tests for auto-detecting JetBrains IDEs and skipping jdtls."""
755
+
756
+ def _make_init_params(self, client_name: str | None = None) -> Any:
757
+ """Create minimal InitializeParams with optional client_info."""
758
+ return lsp.InitializeParams(
759
+ capabilities=lsp.ClientCapabilities(),
760
+ root_uri="file:///tmp/test",
761
+ client_info=lsp.ClientInfo(name=client_name) if client_name else None,
762
+ )
763
+
764
+ def _run_init(self, monkeypatch: Any, client_name: str | None, env_value: str | None = None) -> None:
765
+ """Run on_initialize with controlled env and client_info."""
766
+ from java_functional_lsp.server import on_initialize, server
767
+
768
+ server._skip_jdtls = False
769
+ server._skip_jdtls_registration = False
770
+ if env_value is not None:
771
+ monkeypatch.setenv("JAVA_FUNCTIONAL_LSP_JDTLS", env_value)
772
+ else:
773
+ monkeypatch.delenv("JAVA_FUNCTIONAL_LSP_JDTLS", raising=False)
774
+ on_initialize(self._make_init_params(client_name))
775
+
776
+ def test_intellij_client_info_skips_jdtls(self, monkeypatch: Any) -> None:
777
+ """IntelliJ IDEA detected → _skip_jdtls = True."""
778
+ from java_functional_lsp.server import server
779
+
780
+ self._run_init(monkeypatch, "IntelliJ IDEA")
781
+ assert server._skip_jdtls is True
782
+
783
+ def test_jetbrains_product_skips_jdtls(self, monkeypatch: Any) -> None:
784
+ """Any JetBrains product detected → _skip_jdtls = True."""
785
+ from java_functional_lsp.server import server
786
+
787
+ self._run_init(monkeypatch, "JetBrains Rider 2025.2")
788
+ assert server._skip_jdtls is True
789
+
790
+ def test_vscode_allows_jdtls(self, monkeypatch: Any) -> None:
791
+ """VS Code → _skip_jdtls = False (jdtls needed)."""
792
+ from java_functional_lsp.server import server
793
+
794
+ self._run_init(monkeypatch, "Visual Studio Code")
795
+ assert server._skip_jdtls is False
796
+
797
+ def test_no_client_info_allows_jdtls(self, monkeypatch: Any) -> None:
798
+ """No client_info → _skip_jdtls = False (safe default)."""
799
+ from java_functional_lsp.server import server
800
+
801
+ self._run_init(monkeypatch, None)
802
+ assert server._skip_jdtls is False
803
+
804
+ def test_env_var_off_overrides_detection(self, monkeypatch: Any) -> None:
805
+ """JAVA_FUNCTIONAL_LSP_JDTLS=off disables jdtls even for non-JetBrains client."""
806
+ from java_functional_lsp.server import server
807
+
808
+ self._run_init(monkeypatch, "Visual Studio Code", env_value="off")
809
+ assert server._skip_jdtls is True
810
+
811
+ def test_env_var_on_overrides_intellij_detection(self, monkeypatch: Any) -> None:
812
+ """JAVA_FUNCTIONAL_LSP_JDTLS=on force-enables jdtls even for IntelliJ."""
813
+ from java_functional_lsp.server import server
814
+
815
+ self._run_init(monkeypatch, "IntelliJ IDEA", env_value="on")
816
+ assert server._skip_jdtls is False
817
+
818
+ def test_env_var_no_register(self, monkeypatch: Any) -> None:
819
+ """JAVA_FUNCTIONAL_LSP_JDTLS=no-register keeps jdtls but skips registration."""
820
+ from java_functional_lsp.server import server
821
+
822
+ self._run_init(monkeypatch, "Visual Studio Code", env_value="no-register")
823
+ assert server._skip_jdtls is False
824
+ assert server._skip_jdtls_registration is True
825
+
826
+ def test_custom_diagnostics_publish_when_jdtls_skipped(self) -> None:
827
+ """Tree-sitter analysis works normally when jdtls is skipped."""
828
+ from unittest.mock import patch
829
+
830
+ from java_functional_lsp.server import _run_analysis
831
+
832
+ java_source = "public class Foo { public String bar() { return null; } }"
833
+
834
+ with patch("java_functional_lsp.server.server") as mock_server:
835
+ mock_server._proxy.is_available = False
836
+ mock_server._skip_jdtls = True
837
+ mock_server._config = {}
838
+ mock_server._parser = __import__("java_functional_lsp.analyzers.base", fromlist=["get_parser"]).get_parser()
839
+
840
+ result = _run_analysis(java_source, "file:///test/Foo.java")
841
+
842
+ custom_diags = [d for d in result if d.source == "java-functional-lsp"]
843
+ assert len(custom_diags) > 0, "Custom diagnostics should publish when jdtls is skipped"
844
+ assert any(d.code == "null-return" for d in custom_diags), "null-return diagnostic expected"
845
+
846
+
753
847
  class TestFindLombokJar:
754
848
  """Tests for _find_lombok_jar()."""
755
849
 
@@ -184,7 +184,7 @@ wheels = [
184
184
 
185
185
  [[package]]
186
186
  name = "java-functional-lsp"
187
- version = "0.8.0"
187
+ version = "0.8.1"
188
188
  source = { editable = "." }
189
189
  dependencies = [
190
190
  { name = "pygls" },
@@ -426,7 +426,7 @@ wheels = [
426
426
 
427
427
  [[package]]
428
428
  name = "pytest"
429
- version = "9.0.2"
429
+ version = "9.0.3"
430
430
  source = { registry = "https://pypi.org/simple" }
431
431
  dependencies = [
432
432
  { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -437,9 +437,9 @@ dependencies = [
437
437
  { name = "pygments" },
438
438
  { name = "tomli", marker = "python_full_version < '3.11'" },
439
439
  ]
440
- sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
440
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
441
441
  wheels = [
442
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
442
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
443
443
  ]
444
444
 
445
445
  [[package]]