java-functional-lsp 0.7.7__tar.gz → 0.7.9__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.7 → java_functional_lsp-0.7.9}/PKG-INFO +17 -1
  2. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/README.md +16 -0
  3. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/pyproject.toml +1 -1
  4. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/__init__.py +1 -1
  5. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/proxy.py +91 -14
  6. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/server.py +26 -1
  7. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_proxy.py +2 -2
  8. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_server.py +119 -0
  9. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/uv.lock +1 -1
  10. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.claude-plugin/plugin.json +0 -0
  11. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.githooks/pre-commit +0 -0
  12. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.githooks/pre-push +0 -0
  13. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/CODEOWNERS +0 -0
  14. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  15. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  16. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/SECURITY.md +0 -0
  18. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/dependabot.yml +0 -0
  19. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/release-drafter.yml +0 -0
  20. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/workflows/publish.yml +0 -0
  21. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/workflows/release-drafter.yml +0 -0
  22. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/workflows/stale.yml +0 -0
  23. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/workflows/test.yml +0 -0
  24. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/workflows/update-homebrew.yml +0 -0
  25. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.github/workflows/vscode-ext.yml +0 -0
  26. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/.gitignore +0 -0
  27. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/CONTRIBUTING.md +0 -0
  28. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/LICENSE +0 -0
  29. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/SKILL.md +0 -0
  30. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/commands/lint-java.md +0 -0
  31. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/intellij/README.md +0 -0
  32. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/intellij/lsp4ij-template.json +0 -0
  33. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/vscode/.vscodeignore +0 -0
  34. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/vscode/README.md +0 -0
  35. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/vscode/package-lock.json +0 -0
  36. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/vscode/package.json +0 -0
  37. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/vscode/src/extension.ts +0 -0
  38. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/editors/vscode/tsconfig.json +0 -0
  39. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/hooks/hooks.json +0 -0
  40. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/hooks/java_linter_reminder.py +0 -0
  41. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/scripts/ensure-lsp.sh +0 -0
  42. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/scripts/generate-formula.py +0 -0
  43. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/__main__.py +0 -0
  44. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  45. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/analyzers/base.py +0 -0
  46. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  47. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  48. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  49. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  50. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  51. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/cli.py +0 -0
  52. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/src/java_functional_lsp/fixes.py +0 -0
  53. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/__init__.py +0 -0
  54. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/conftest.py +0 -0
  55. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_base.py +0 -0
  56. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_cli.py +0 -0
  57. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_config.py +0 -0
  58. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_e2e.py +0 -0
  59. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_e2e_jdtls.py +0 -0
  60. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_exception_checker.py +0 -0
  61. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_fixes.py +0 -0
  62. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_functional_checker.py +0 -0
  63. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_mutation_checker.py +0 -0
  64. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_null_checker.py +0 -0
  65. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/tests/test_spring_checker.py +0 -0
  66. {java_functional_lsp-0.7.7 → java_functional_lsp-0.7.9}/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.7
3
+ Version: 0.7.9
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
@@ -253,6 +253,22 @@ public String legacyMethod() { ... }
253
253
 
254
254
  Works on classes, methods, constructors, fields, and local variables. Suppression applies to the annotated scope — a class-level annotation suppresses all methods within it.
255
255
 
256
+ ### Lombok support
257
+
258
+ Projects using [Lombok](https://projectlombok.org/) need the Lombok Java agent for jdtls to process `@Builder`, `@Value`, `@Data`, `@Slf4j`, and other annotations. Without it, jdtls reports false "method undefined" errors for generated methods.
259
+
260
+ The server auto-discovers `lombok.jar` from these locations (first match wins):
261
+
262
+ 1. **Project config** — add to `.java-functional-lsp.json`:
263
+ ```json
264
+ { "lombok": "/path/to/lombok.jar" }
265
+ ```
266
+ 2. **Environment variable** — `LOMBOK_JAR=/path/to/lombok.jar`
267
+ 3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
268
+ 4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
269
+
270
+ If Lombok is used in your project but the jar isn't found, the server automatically filters common Lombok false-positive errors (like "builder() is undefined") so they don't clutter your diagnostics.
271
+
256
272
  ## Code actions (quick fixes)
257
273
 
258
274
  The server provides LSP code actions (`textDocument/codeAction`) that automatically refactor code. When your editor shows a diagnostic with a lightbulb icon, clicking it applies the fix:
@@ -225,6 +225,22 @@ public String legacyMethod() { ... }
225
225
 
226
226
  Works on classes, methods, constructors, fields, and local variables. Suppression applies to the annotated scope — a class-level annotation suppresses all methods within it.
227
227
 
228
+ ### Lombok support
229
+
230
+ Projects using [Lombok](https://projectlombok.org/) need the Lombok Java agent for jdtls to process `@Builder`, `@Value`, `@Data`, `@Slf4j`, and other annotations. Without it, jdtls reports false "method undefined" errors for generated methods.
231
+
232
+ The server auto-discovers `lombok.jar` from these locations (first match wins):
233
+
234
+ 1. **Project config** — add to `.java-functional-lsp.json`:
235
+ ```json
236
+ { "lombok": "/path/to/lombok.jar" }
237
+ ```
238
+ 2. **Environment variable** — `LOMBOK_JAR=/path/to/lombok.jar`
239
+ 3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
240
+ 4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
241
+
242
+ If Lombok is used in your project but the jar isn't found, the server automatically filters common Lombok false-positive errors (like "builder() is undefined") so they don't clutter your diagnostics.
243
+
228
244
  ## Code actions (quick fixes)
229
245
 
230
246
  The server provides LSP code actions (`textDocument/codeAction`) that automatically refactor code. When your editor shows a diagnostic with a lightbulb icon, clicking it applies the fix:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.7.7"
7
+ version = "0.7.9"
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.7"
3
+ __version__ = "0.7.9"
@@ -433,6 +433,58 @@ def _resolve_module_uri(file_uri: str) -> str | None:
433
433
  return module_uri or None
434
434
 
435
435
 
436
+ def _version_key(name: str) -> tuple[int, ...]:
437
+ """Parse a version string into a tuple for semantic comparison."""
438
+ parts: list[int] = []
439
+ for segment in re.split(r"[.\-]", name):
440
+ try:
441
+ parts.append(int(segment))
442
+ except ValueError:
443
+ break
444
+ return tuple(parts)
445
+
446
+
447
+ def _find_lombok_jar(config: Mapping[str, Any] | None = None) -> str | None:
448
+ """Find lombok.jar from configurable and auto-discovered locations.
449
+
450
+ Search order (first match wins):
451
+ 1. Project config (.java-functional-lsp.json): ``{"lombok": "/path/to/lombok.jar"}``
452
+ 2. Environment variable: ``LOMBOK_JAR``
453
+ 3. Maven cache: ``~/.m2/repository/org/projectlombok/lombok/*/lombok-*.jar``
454
+ 4. Dedicated directory: ``~/.jdtls-libs/lombok.jar``
455
+ """
456
+ # 1. Project config
457
+ if config and config.get("lombok"):
458
+ p = Path(config["lombok"]).expanduser()
459
+ if p.is_file():
460
+ return str(p)
461
+ logger.warning("Lombok path from config does not exist: %s", _redact_path(str(p)))
462
+
463
+ # 2. Environment variable
464
+ env_path = os.environ.get("LOMBOK_JAR")
465
+ if env_path:
466
+ p = Path(env_path).expanduser()
467
+ if p.is_file():
468
+ return str(p)
469
+ logger.warning("LOMBOK_JAR does not exist: %s", _redact_path(env_path))
470
+
471
+ # 3. Maven cache (latest version, semantic sort)
472
+ m2_lombok = Path.home() / ".m2" / "repository" / "org" / "projectlombok" / "lombok"
473
+ if m2_lombok.is_dir():
474
+ version_dirs = [d for d in m2_lombok.iterdir() if d.is_dir()]
475
+ for version_dir in sorted(version_dirs, key=lambda d: _version_key(d.name), reverse=True):
476
+ jar = version_dir / f"lombok-{version_dir.name}.jar"
477
+ if jar.is_file():
478
+ return str(jar)
479
+
480
+ # 4. Dedicated directory
481
+ fallback = Path.home() / ".jdtls-libs" / "lombok.jar"
482
+ if fallback.is_file():
483
+ return str(fallback)
484
+
485
+ return None
486
+
487
+
436
488
  class JdtlsProxy:
437
489
  """Manages a jdtls subprocess and provides async request/notification forwarding."""
438
490
 
@@ -457,6 +509,7 @@ class JdtlsProxy:
457
509
  self._original_root_uri: str | None = None
458
510
  self._initial_module_uri: str | None = None
459
511
  self.modules = ModuleRegistry()
512
+ self.has_lombok = False
460
513
  self._workspace_expanded = False
461
514
 
462
515
  @property
@@ -480,22 +533,32 @@ class JdtlsProxy:
480
533
  logger.warning("jdtls not found on PATH — running in standalone mode (custom rules only)")
481
534
  return self._jdtls_on_path
482
535
 
483
- async def start(self, init_params: dict[str, Any], *, module_root_uri: str | None = None) -> bool:
536
+ async def start(
537
+ self,
538
+ init_params: dict[str, Any],
539
+ *,
540
+ module_root_uri: str | None = None,
541
+ config: Mapping[str, Any] | None = None,
542
+ ) -> bool:
484
543
  """Start jdtls subprocess and initialize it.
485
544
 
486
545
  If *module_root_uri* is provided, jdtls is scoped to that module for
487
- fast startup. The data-directory hash is always based on the original
488
- workspace root (from init_params) so the index persists across restarts.
546
+ fast startup and the data-dir hash is based on the module URI (so each
547
+ module gets its own isolated index). Otherwise the workspace root is used.
489
548
  """
490
549
  jdtls_path = shutil.which("jdtls")
491
550
  if not jdtls_path:
492
551
  return False
493
552
 
494
- # Data-dir hash based on original workspace root (stable across module changes).
553
+ # Data-dir hash: use module URI when scoped so each module gets its own
554
+ # clean jdtls state. This avoids loading a 2.5GB monorepo index when only
555
+ # one module is needed. Without expansion, each session is module-scoped,
556
+ # so the data-dir should match that scope.
495
557
  original_root: str = init_params.get("rootUri") or init_params.get("rootPath") or str(Path.cwd())
496
558
  self._original_root_uri = original_root
497
- workspace_hash = hashlib.sha256(original_root.encode()).hexdigest()[:12]
498
- data_dir = Path.home() / ".cache" / "jdtls-data" / workspace_hash
559
+ hash_source = module_root_uri or original_root
560
+ data_hash = hashlib.sha256(hash_source.encode()).hexdigest()[:12]
561
+ data_dir = Path.home() / ".cache" / "jdtls-data" / data_hash
499
562
  data_dir.mkdir(parents=True, exist_ok=True)
500
563
 
501
564
  # Deep copy to avoid mutating server._init_params.
@@ -521,16 +584,24 @@ class JdtlsProxy:
521
584
  self._initial_module_uri = module_root_uri
522
585
  self.modules.mark_added(effective_root_uri)
523
586
 
524
- # Build a clean environment for jdtls.
587
+ # Build a clean environment and find Lombok jar (both do blocking I/O).
525
588
  loop = asyncio.get_running_loop()
526
- jdtls_env = await loop.run_in_executor(None, build_jdtls_env)
589
+ jdtls_env, lombok_jar = await loop.run_in_executor(None, lambda: (build_jdtls_env(), _find_lombok_jar(config)))
590
+ if lombok_jar:
591
+ logger.info("jdtls: using Lombok agent from %s", _redact_path(lombok_jar))
592
+
593
+ jdtls_cmd: list[str] = [
594
+ jdtls_path,
595
+ "-data",
596
+ str(data_dir),
597
+ f"--jvm-arg=-Xmx{DEFAULT_JVM_MAX_HEAP}",
598
+ ]
599
+ if lombok_jar:
600
+ jdtls_cmd.append(f"--jvm-arg=-javaagent:{lombok_jar}")
527
601
 
528
602
  try:
529
603
  self._process = await asyncio.create_subprocess_exec(
530
- jdtls_path,
531
- "-data",
532
- str(data_dir),
533
- f"--jvm-arg=-Xmx{DEFAULT_JVM_MAX_HEAP}",
604
+ *jdtls_cmd,
534
605
  stdin=asyncio.subprocess.PIPE,
535
606
  stdout=asyncio.subprocess.PIPE,
536
607
  stderr=asyncio.subprocess.PIPE,
@@ -559,13 +630,19 @@ class JdtlsProxy:
559
630
 
560
631
  await self.send_notification("initialized", {})
561
632
  self._available = True
633
+ self.has_lombok = lombok_jar is not None
562
634
  return True
563
635
 
564
636
  except (OSError, FileNotFoundError) as e:
565
637
  logger.error("Failed to start jdtls: %s", e)
566
638
  return False
567
639
 
568
- async def ensure_started(self, init_params: dict[str, Any], file_uri: str) -> bool:
640
+ async def ensure_started(
641
+ self,
642
+ init_params: dict[str, Any],
643
+ file_uri: str,
644
+ config: Mapping[str, Any] | None = None,
645
+ ) -> bool:
569
646
  """Start jdtls lazily, scoped to the module containing *file_uri*.
570
647
 
571
648
  Thread-safe: uses asyncio.Lock to prevent double-start from rapid
@@ -592,7 +669,7 @@ class JdtlsProxy:
592
669
  self._starting = True
593
670
  try:
594
671
  module_uri = _resolve_module_uri(file_uri)
595
- started = await self.start(init_params, module_root_uri=module_uri)
672
+ started = await self.start(init_params, module_root_uri=module_uri, config=config)
596
673
  if not started:
597
674
  self._start_failed = True
598
675
  self._start_failed_at = time.monotonic()
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import asyncio
10
10
  import json
11
11
  import logging
12
+ import re
12
13
  import sys
13
14
  from pathlib import Path
14
15
  from typing import Any
@@ -197,6 +198,28 @@ def _jdtls_raw_to_lsp_diagnostics(raw_diagnostics: list[Any]) -> list[lsp.Diagno
197
198
  return result
198
199
 
199
200
 
201
+ # Lombok-specific patterns: only match diagnostics that are clearly Lombok-generated.
202
+ # Intentionally narrow to avoid suppressing real compile errors.
203
+ _LOMBOK_PATTERNS = re.compile(
204
+ r"(?:"
205
+ r"The method builder\(\)|" # @Builder
206
+ r"The method toBuilder\(\)|" # @Builder(toBuilder=true)
207
+ r"log cannot be resolved|" # @Slf4j, @Log
208
+ r"\w+Builder cannot be resolved to a type|" # @Builder inner class
209
+ r"The blank final field \w+ may not have been initialized" # @Value final fields
210
+ r")"
211
+ )
212
+
213
+
214
+ def _is_lombok_false_positive(diag: dict[str, Any]) -> bool:
215
+ """Check if a jdtls diagnostic is likely a Lombok false positive.
216
+
217
+ Only matches narrow Lombok-specific patterns to avoid suppressing real errors.
218
+ """
219
+ msg = diag.get("message", "")
220
+ return bool(_LOMBOK_PATTERNS.search(msg))
221
+
222
+
200
223
  def _run_analysis(source: str, uri: str) -> list[lsp.Diagnostic]:
201
224
  """Run custom analyzers on source text and merge with jdtls diagnostics."""
202
225
  custom_diags = _analyze_document(source, uri)
@@ -204,6 +227,8 @@ def _run_analysis(source: str, uri: str) -> list[lsp.Diagnostic]:
204
227
  jdtls_diags: list[lsp.Diagnostic] = []
205
228
  if server._proxy.is_available:
206
229
  raw = server._proxy.get_cached_diagnostics(uri)
230
+ if not server._proxy.has_lombok:
231
+ raw = [d for d in raw if not _is_lombok_false_positive(d)]
207
232
  jdtls_diags = _jdtls_raw_to_lsp_diagnostics(raw)
208
233
 
209
234
  return jdtls_diags + custom_diags
@@ -419,7 +444,7 @@ async def _lazy_start_jdtls(file_uri: str) -> None:
419
444
  are loaded on-demand via ``add_module_if_new()`` as files are opened.
420
445
  """
421
446
  try:
422
- started = await server._proxy.ensure_started(server._init_params, file_uri)
447
+ started = await server._proxy.ensure_started(server._init_params, file_uri, config=server._config)
423
448
  if started:
424
449
  logger.info("jdtls proxy active — full Java language support enabled")
425
450
  await _register_jdtls_capabilities()
@@ -1103,7 +1103,7 @@ class TestLazyStart:
1103
1103
  proxy._jdtls_on_path = True
1104
1104
  captured: dict[str, Any] = {}
1105
1105
 
1106
- async def capturing_start(params: Any, *, module_root_uri: str | None = None) -> bool:
1106
+ async def capturing_start(params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1107
1107
  captured["module_root_uri"] = module_root_uri
1108
1108
  return False
1109
1109
 
@@ -1127,7 +1127,7 @@ class TestLazyStart:
1127
1127
 
1128
1128
  captured: dict[str, Any] = {}
1129
1129
 
1130
- async def capturing_start(params: Any, *, module_root_uri: str | None = None) -> bool:
1130
+ async def capturing_start(params: Any, *, module_root_uri: str | None = None, config: Any = None) -> bool:
1131
1131
  captured["module_root_uri"] = module_root_uri
1132
1132
  return False
1133
1133
 
@@ -642,6 +642,125 @@ class TestServerInternals:
642
642
 
643
643
 
644
644
  # --------------------------------------------------------------------------
645
+ # --------------------------------------------------------------------------
646
+ # Lombok support tests
647
+ # --------------------------------------------------------------------------
648
+
649
+
650
+ class TestLombokFalsePositive:
651
+ """Tests for _is_lombok_false_positive()."""
652
+
653
+ def test_matches_builder_undefined(self) -> None:
654
+ from java_functional_lsp.server import _is_lombok_false_positive
655
+
656
+ diag = {"code": "67108964", "message": "The method builder() is undefined for the type Foo"}
657
+ assert _is_lombok_false_positive(diag) is True
658
+
659
+ def test_matches_log_unresolved(self) -> None:
660
+ from java_functional_lsp.server import _is_lombok_false_positive
661
+
662
+ diag = {"code": "570425394", "message": "log cannot be resolved"}
663
+ assert _is_lombok_false_positive(diag) is True
664
+
665
+ def test_matches_builder_type_unresolved(self) -> None:
666
+ from java_functional_lsp.server import _is_lombok_false_positive
667
+
668
+ diag = {"code": "16777218", "message": "FooBuilder cannot be resolved to a type"}
669
+ assert _is_lombok_false_positive(diag) is True
670
+
671
+ def test_does_not_match_real_undefined_method(self) -> None:
672
+ from java_functional_lsp.server import _is_lombok_false_positive
673
+
674
+ diag = {"code": "67108964", "message": "The method foo() is undefined for the type String"}
675
+ assert _is_lombok_false_positive(diag) is False
676
+
677
+ def test_does_not_match_real_unresolved_type(self) -> None:
678
+ from java_functional_lsp.server import _is_lombok_false_positive
679
+
680
+ diag = {"code": "16777218", "message": "SomeClass cannot be resolved to a type"}
681
+ assert _is_lombok_false_positive(diag) is False
682
+
683
+ def test_empty_message(self) -> None:
684
+ from java_functional_lsp.server import _is_lombok_false_positive
685
+
686
+ diag = {"code": "67108964", "message": ""}
687
+ assert _is_lombok_false_positive(diag) is False
688
+
689
+ def test_missing_fields(self) -> None:
690
+ from java_functional_lsp.server import _is_lombok_false_positive
691
+
692
+ assert _is_lombok_false_positive({}) is False
693
+
694
+ def test_matches_blank_final_field(self) -> None:
695
+ from java_functional_lsp.server import _is_lombok_false_positive
696
+
697
+ diag = {"code": "33554513", "message": "The blank final field name may not have been initialized"}
698
+ assert _is_lombok_false_positive(diag) is True
699
+
700
+
701
+ class TestFindLombokJar:
702
+ """Tests for _find_lombok_jar()."""
703
+
704
+ def test_config_path_wins(self, tmp_path: Any) -> None:
705
+ from java_functional_lsp.proxy import _find_lombok_jar
706
+
707
+ jar = tmp_path / "lombok.jar"
708
+ jar.touch()
709
+ result = _find_lombok_jar({"lombok": str(jar)})
710
+ assert result == str(jar)
711
+
712
+ def test_env_var_used(self, tmp_path: Any, monkeypatch: Any) -> None:
713
+ from java_functional_lsp.proxy import _find_lombok_jar
714
+
715
+ jar = tmp_path / "lombok.jar"
716
+ jar.touch()
717
+ monkeypatch.setenv("LOMBOK_JAR", str(jar))
718
+ result = _find_lombok_jar()
719
+ assert result == str(jar)
720
+
721
+ def test_config_wins_over_env(self, tmp_path: Any, monkeypatch: Any) -> None:
722
+ from java_functional_lsp.proxy import _find_lombok_jar
723
+
724
+ config_jar = tmp_path / "config-lombok.jar"
725
+ config_jar.touch()
726
+ env_jar = tmp_path / "env-lombok.jar"
727
+ env_jar.touch()
728
+ monkeypatch.setenv("LOMBOK_JAR", str(env_jar))
729
+ result = _find_lombok_jar({"lombok": str(config_jar)})
730
+ assert result == str(config_jar)
731
+
732
+ def test_maven_cache_semantic_sort(self, tmp_path: Any, monkeypatch: Any) -> None:
733
+ from java_functional_lsp.proxy import _find_lombok_jar
734
+
735
+ monkeypatch.delenv("LOMBOK_JAR", raising=False)
736
+ m2 = tmp_path / ".m2" / "repository" / "org" / "projectlombok" / "lombok"
737
+ for v in ["1.18.4", "1.18.30", "1.9.2"]:
738
+ d = m2 / v
739
+ d.mkdir(parents=True)
740
+ (d / f"lombok-{v}.jar").touch()
741
+ monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
742
+ result = _find_lombok_jar()
743
+ assert result is not None
744
+ assert "1.18.30" in result # Semantic sort picks 1.18.30 over 1.9.2
745
+
746
+ def test_returns_none_when_nothing_found(self, tmp_path: Any, monkeypatch: Any) -> None:
747
+ from java_functional_lsp.proxy import _find_lombok_jar
748
+
749
+ monkeypatch.delenv("LOMBOK_JAR", raising=False)
750
+ monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
751
+ result = _find_lombok_jar()
752
+ assert result is None
753
+
754
+ def test_warns_on_missing_config_path(self, tmp_path: Any, caplog: Any) -> None:
755
+ import logging
756
+
757
+ from java_functional_lsp.proxy import _find_lombok_jar
758
+
759
+ with caplog.at_level(logging.WARNING, logger="java_functional_lsp.proxy"):
760
+ _find_lombok_jar({"lombok": "/nonexistent/lombok.jar"})
761
+ assert any("does not exist" in r.getMessage() for r in caplog.records)
762
+
763
+
645
764
  # Subprocess-based tests — zero mocks, real LSP transport
646
765
  # --------------------------------------------------------------------------
647
766
 
@@ -184,7 +184,7 @@ wheels = [
184
184
 
185
185
  [[package]]
186
186
  name = "java-functional-lsp"
187
- version = "0.7.4"
187
+ version = "0.7.9"
188
188
  source = { editable = "." }
189
189
  dependencies = [
190
190
  { name = "pygls" },