java-functional-lsp 0.7.9__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.
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/PKG-INFO +20 -2
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/README.md +19 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/intellij/README.md +2 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/pyproject.toml +1 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/proxy.py +44 -16
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/server.py +121 -26
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_server.py +224 -33
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/uv.lock +4 -4
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.gitignore +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/LICENSE +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/SKILL.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/base.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/fixes.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/__init__.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/conftest.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_base.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_config.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_e2e_jdtls.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_functional_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_mutation_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_proxy.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.7.9 → 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.
|
|
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
|
|
@@ -230,6 +232,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
230
232
|
**Options:**
|
|
231
233
|
- `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
|
|
232
234
|
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
235
|
+
- `suppressJdtlsPatterns` — list of regex patterns to suppress jdtls diagnostics (see below)
|
|
233
236
|
|
|
234
237
|
**Spring-aware behavior:**
|
|
235
238
|
- `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
|
|
@@ -267,7 +270,22 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
267
270
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
268
271
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
269
272
|
|
|
270
|
-
If Lombok is used in your project but the jar isn't found, the server
|
|
273
|
+
If Lombok is used in your project but the jar isn't found, the server logs a warning. The jdtls cache is automatically cleared on version upgrade, so adding Lombok support takes effect immediately after `brew upgrade`.
|
|
274
|
+
|
|
275
|
+
### Suppressing jdtls diagnostics
|
|
276
|
+
|
|
277
|
+
For project-specific jdtls false positives (e.g., annotation processor methods, MapStruct mappers), use `suppressJdtlsPatterns` to add custom regex filters:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"suppressJdtlsPatterns": [
|
|
282
|
+
"The method \\w+Mapper\\(\\) is undefined",
|
|
283
|
+
"cannot be resolved to a type"
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Each entry is a regex matched against jdtls diagnostic messages. Invalid patterns are skipped with a warning.
|
|
271
289
|
|
|
272
290
|
## Code actions (quick fixes)
|
|
273
291
|
|
|
@@ -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
|
|
@@ -202,6 +204,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
202
204
|
**Options:**
|
|
203
205
|
- `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
|
|
204
206
|
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
207
|
+
- `suppressJdtlsPatterns` — list of regex patterns to suppress jdtls diagnostics (see below)
|
|
205
208
|
|
|
206
209
|
**Spring-aware behavior:**
|
|
207
210
|
- `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
|
|
@@ -239,7 +242,22 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
239
242
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
240
243
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
241
244
|
|
|
242
|
-
If Lombok is used in your project but the jar isn't found, the server
|
|
245
|
+
If Lombok is used in your project but the jar isn't found, the server logs a warning. The jdtls cache is automatically cleared on version upgrade, so adding Lombok support takes effect immediately after `brew upgrade`.
|
|
246
|
+
|
|
247
|
+
### Suppressing jdtls diagnostics
|
|
248
|
+
|
|
249
|
+
For project-specific jdtls false positives (e.g., annotation processor methods, MapStruct mappers), use `suppressJdtlsPatterns` to add custom regex filters:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"suppressJdtlsPatterns": [
|
|
254
|
+
"The method \\w+Mapper\\(\\) is undefined",
|
|
255
|
+
"cannot be resolved to a type"
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Each entry is a regex matched against jdtls diagnostic messages. Invalid patterns are skipped with a warning.
|
|
243
261
|
|
|
244
262
|
## Code actions (quick fixes)
|
|
245
263
|
|
|
@@ -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
|
-
|
|
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.
|
|
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" }
|
|
@@ -444,6 +444,29 @@ def _version_key(name: str) -> tuple[int, ...]:
|
|
|
444
444
|
return tuple(parts)
|
|
445
445
|
|
|
446
446
|
|
|
447
|
+
def _clear_cache_on_version_change(cache_root: Path) -> None:
|
|
448
|
+
"""Clear jdtls data cache when the server version changes.
|
|
449
|
+
|
|
450
|
+
Stores a ``.version`` marker in the cache root. On version mismatch
|
|
451
|
+
(e.g., Lombok agent added, jdtls config changed), wipes the entire
|
|
452
|
+
cache so jdtls re-indexes from scratch with the current configuration.
|
|
453
|
+
"""
|
|
454
|
+
from . import __version__
|
|
455
|
+
|
|
456
|
+
marker = cache_root / ".version"
|
|
457
|
+
try:
|
|
458
|
+
if marker.exists() and marker.read_text().strip() == __version__:
|
|
459
|
+
return
|
|
460
|
+
# Version changed or first run — clear stale caches.
|
|
461
|
+
if cache_root.exists():
|
|
462
|
+
shutil.rmtree(cache_root)
|
|
463
|
+
logger.info("jdtls: cleared stale cache (version changed to %s)", __version__)
|
|
464
|
+
cache_root.mkdir(parents=True, exist_ok=True)
|
|
465
|
+
marker.write_text(__version__)
|
|
466
|
+
except OSError as e:
|
|
467
|
+
logger.warning("jdtls: failed to manage cache version marker: %s", e)
|
|
468
|
+
|
|
469
|
+
|
|
447
470
|
def _find_lombok_jar(config: Mapping[str, Any] | None = None) -> str | None:
|
|
448
471
|
"""Find lombok.jar from configurable and auto-discovered locations.
|
|
449
472
|
|
|
@@ -550,20 +573,35 @@ class JdtlsProxy:
|
|
|
550
573
|
if not jdtls_path:
|
|
551
574
|
return False
|
|
552
575
|
|
|
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.
|
|
557
576
|
original_root: str = init_params.get("rootUri") or init_params.get("rootPath") or str(Path.cwd())
|
|
558
577
|
self._original_root_uri = original_root
|
|
578
|
+
effective_root_uri = module_root_uri or original_root
|
|
579
|
+
|
|
580
|
+
# Mark ADDED before any await to prevent duplicate module additions
|
|
581
|
+
# from concurrent coroutines during the executor yield below.
|
|
582
|
+
self._initial_module_uri = module_root_uri
|
|
583
|
+
self.modules.mark_added(effective_root_uri)
|
|
584
|
+
|
|
585
|
+
# All blocking startup I/O in a single executor call: cache version check
|
|
586
|
+
# (may rmtree on upgrade), Lombok discovery, and jdtls env build.
|
|
587
|
+
cache_root = Path.home() / ".cache" / "jdtls-data"
|
|
588
|
+
|
|
589
|
+
def _blocking_startup() -> tuple[dict[str, str], str | None]:
|
|
590
|
+
_clear_cache_on_version_change(cache_root)
|
|
591
|
+
return build_jdtls_env(), _find_lombok_jar(config)
|
|
592
|
+
|
|
593
|
+
loop = asyncio.get_running_loop()
|
|
594
|
+
jdtls_env, lombok_jar = await loop.run_in_executor(None, _blocking_startup)
|
|
595
|
+
if lombok_jar:
|
|
596
|
+
logger.info("jdtls: using Lombok agent from %s", _redact_path(lombok_jar))
|
|
597
|
+
|
|
559
598
|
hash_source = module_root_uri or original_root
|
|
560
599
|
data_hash = hashlib.sha256(hash_source.encode()).hexdigest()[:12]
|
|
561
|
-
data_dir =
|
|
600
|
+
data_dir = cache_root / data_hash
|
|
562
601
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
563
602
|
|
|
564
603
|
# Deep copy to avoid mutating server._init_params.
|
|
565
604
|
effective_params = copy.deepcopy(init_params)
|
|
566
|
-
effective_root_uri = module_root_uri or original_root
|
|
567
605
|
if module_root_uri:
|
|
568
606
|
effective_params["rootUri"] = module_root_uri
|
|
569
607
|
from pygls.uris import to_fs_path
|
|
@@ -580,16 +618,6 @@ class JdtlsProxy:
|
|
|
580
618
|
ws = caps.setdefault("workspace", {})
|
|
581
619
|
ws["workspaceFolders"] = True
|
|
582
620
|
|
|
583
|
-
# Track the initial module as already loaded (mark ADDED before await).
|
|
584
|
-
self._initial_module_uri = module_root_uri
|
|
585
|
-
self.modules.mark_added(effective_root_uri)
|
|
586
|
-
|
|
587
|
-
# Build a clean environment and find Lombok jar (both do blocking I/O).
|
|
588
|
-
loop = asyncio.get_running_loop()
|
|
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
621
|
jdtls_cmd: list[str] = [
|
|
594
622
|
jdtls_path,
|
|
595
623
|
"-data",
|
|
@@ -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
|
|
@@ -65,6 +70,10 @@ class JavaFunctionalLspServer(LanguageServer):
|
|
|
65
70
|
self._config: dict[str, Any] = {}
|
|
66
71
|
self._init_params: dict[str, Any] = {}
|
|
67
72
|
self._proxy = JdtlsProxy(on_diagnostics=self._on_jdtls_diagnostics)
|
|
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
|
|
68
77
|
|
|
69
78
|
def _on_jdtls_diagnostics(self, uri: str, diagnostics: list[Any]) -> None:
|
|
70
79
|
"""Called when jdtls publishes diagnostics — merge with custom and re-publish.
|
|
@@ -198,38 +207,60 @@ def _jdtls_raw_to_lsp_diagnostics(raw_diagnostics: list[Any]) -> list[lsp.Diagno
|
|
|
198
207
|
return result
|
|
199
208
|
|
|
200
209
|
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
_MAX_PATTERN_LENGTH = 500 # Cap regex length to mitigate ReDoS from pathological patterns
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _compile_user_patterns(config: dict[str, Any]) -> list[re.Pattern[str]]:
|
|
214
|
+
"""Compile user-defined suppressJdtlsPatterns from config."""
|
|
215
|
+
raw = config.get("suppressJdtlsPatterns", [])
|
|
216
|
+
if not isinstance(raw, list):
|
|
217
|
+
logger.warning("suppressJdtlsPatterns must be a list of regex strings, got %s", type(raw).__name__)
|
|
218
|
+
return []
|
|
219
|
+
patterns: list[re.Pattern[str]] = []
|
|
220
|
+
for entry in raw:
|
|
221
|
+
if not isinstance(entry, str):
|
|
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
|
|
226
|
+
try:
|
|
227
|
+
patterns.append(re.compile(entry))
|
|
228
|
+
except re.error as e:
|
|
229
|
+
logger.warning("Invalid suppressJdtlsPatterns regex %r: %s", entry, e)
|
|
230
|
+
return patterns
|
|
212
231
|
|
|
213
232
|
|
|
214
|
-
def
|
|
215
|
-
"""Check if a jdtls diagnostic
|
|
233
|
+
def _is_jdtls_suppressed(diag: dict[str, Any], user_patterns: list[re.Pattern[str]]) -> bool:
|
|
234
|
+
"""Check if a jdtls diagnostic matches a user-configured suppress pattern.
|
|
216
235
|
|
|
217
|
-
|
|
236
|
+
No built-in patterns — the root cause (stale jdtls caches) is fixed by
|
|
237
|
+
clearing the cache on version change. Users can still suppress specific
|
|
238
|
+
jdtls messages via suppressJdtlsPatterns in .java-functional-lsp.json.
|
|
218
239
|
"""
|
|
240
|
+
if not user_patterns:
|
|
241
|
+
return False
|
|
219
242
|
msg = diag.get("message", "")
|
|
220
|
-
|
|
243
|
+
for pat in user_patterns:
|
|
244
|
+
if pat.search(msg):
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
221
247
|
|
|
222
248
|
|
|
223
249
|
def _run_analysis(source: str, uri: str) -> list[lsp.Diagnostic]:
|
|
224
|
-
"""Run custom analyzers on source text and merge with jdtls diagnostics.
|
|
250
|
+
"""Run custom analyzers on source text and merge with jdtls diagnostics.
|
|
251
|
+
|
|
252
|
+
jdtls processing is isolated: if it fails, custom diagnostics still publish.
|
|
253
|
+
"""
|
|
225
254
|
custom_diags = _analyze_document(source, uri)
|
|
226
255
|
|
|
227
256
|
jdtls_diags: list[lsp.Diagnostic] = []
|
|
228
257
|
if server._proxy.is_available:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
raw = [d for d in raw if not
|
|
232
|
-
|
|
258
|
+
try:
|
|
259
|
+
raw = server._proxy.get_cached_diagnostics(uri)
|
|
260
|
+
raw = [d for d in raw if not _is_jdtls_suppressed(d, server._user_suppress_patterns)]
|
|
261
|
+
jdtls_diags = _jdtls_raw_to_lsp_diagnostics(raw)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.warning("jdtls diagnostic processing failed for %s: %s", uri, e)
|
|
233
264
|
|
|
234
265
|
return jdtls_diags + custom_diags
|
|
235
266
|
|
|
@@ -257,6 +288,39 @@ def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|
|
257
288
|
root = params.root_path
|
|
258
289
|
|
|
259
290
|
server._config = _load_config(root)
|
|
291
|
+
server._user_suppress_patterns = _compile_user_patterns(server._config)
|
|
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
|
+
)
|
|
260
324
|
|
|
261
325
|
return lsp.InitializeResult(
|
|
262
326
|
capabilities=lsp.ServerCapabilities(
|
|
@@ -284,6 +348,9 @@ async def on_initialized(params: lsp.InitializedParams) -> None:
|
|
|
284
348
|
"java-functional-lsp initialized (rules: %s)",
|
|
285
349
|
list(server._config.get("rules", {}).keys()) or "all defaults",
|
|
286
350
|
)
|
|
351
|
+
if server._skip_jdtls:
|
|
352
|
+
logger.info("jdtls proxy disabled — custom rules only")
|
|
353
|
+
return
|
|
287
354
|
if server._proxy.check_available():
|
|
288
355
|
logger.info("jdtls found on PATH — will start lazily on first file open")
|
|
289
356
|
else:
|
|
@@ -331,11 +398,15 @@ async def _register_jdtls_capabilities() -> None:
|
|
|
331
398
|
ready, which suppresses the IDE's built-in diagnostic tooltips.
|
|
332
399
|
|
|
333
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).
|
|
334
403
|
"""
|
|
335
404
|
global _jdtls_capabilities_registered
|
|
336
405
|
if _jdtls_capabilities_registered:
|
|
337
406
|
return
|
|
338
407
|
|
|
408
|
+
generation = server._init_generation
|
|
409
|
+
|
|
339
410
|
try:
|
|
340
411
|
# Register handlers so pygls dispatches incoming requests to them.
|
|
341
412
|
for method, handler in _JDTLS_HANDLERS.items():
|
|
@@ -344,6 +415,12 @@ async def _register_jdtls_capabilities() -> None:
|
|
|
344
415
|
# Tell the client we now support these capabilities.
|
|
345
416
|
registrations = _build_jdtls_registrations()
|
|
346
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
|
+
|
|
347
424
|
_jdtls_capabilities_registered = True
|
|
348
425
|
logger.info("Dynamically registered jdtls capabilities (hover, definition, references, completion, symbol)")
|
|
349
426
|
except Exception:
|
|
@@ -363,11 +440,16 @@ def _analyze_and_publish(uri: str) -> None:
|
|
|
363
440
|
async def _deferred_validate(uri: str) -> None:
|
|
364
441
|
"""Debounced validation — waits before analyzing to batch rapid edits."""
|
|
365
442
|
await asyncio.sleep(_DEBOUNCE_SECONDS)
|
|
366
|
-
|
|
443
|
+
try:
|
|
444
|
+
_analyze_and_publish(uri)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
logger.error("Validation failed for %s: %s", uri, e)
|
|
367
447
|
|
|
368
448
|
|
|
369
449
|
def _forward_or_queue(method: str, serialized: Any) -> None:
|
|
370
450
|
"""Forward a notification to jdtls if available, or queue it if starting."""
|
|
451
|
+
if server._skip_jdtls:
|
|
452
|
+
return
|
|
371
453
|
if server._proxy.is_available:
|
|
372
454
|
task = asyncio.create_task(server._proxy.send_notification(method, serialized))
|
|
373
455
|
_bg_tasks.add(task)
|
|
@@ -385,14 +467,18 @@ async def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
|
|
385
467
|
didOpen response isn't delayed by jdtls cold-start.
|
|
386
468
|
"""
|
|
387
469
|
uri = params.text_document.uri
|
|
388
|
-
serialized = _serialize_params(params)
|
|
389
470
|
|
|
390
|
-
if server.
|
|
471
|
+
if server._skip_jdtls:
|
|
472
|
+
# Skip all jdtls forwarding — custom diagnostics only.
|
|
473
|
+
pass
|
|
474
|
+
elif server._proxy.is_available:
|
|
391
475
|
# Fast path: jdtls running. Forward didOpen + add module if new.
|
|
476
|
+
serialized = _serialize_params(params)
|
|
392
477
|
await server._proxy.send_notification("textDocument/didOpen", serialized)
|
|
393
478
|
await server._proxy.add_module_if_new(uri)
|
|
394
479
|
elif server._proxy._jdtls_on_path and not server._proxy._start_failed:
|
|
395
480
|
# Queue the didOpen (whether this is the first file or a subsequent one during startup).
|
|
481
|
+
serialized = _serialize_params(params)
|
|
396
482
|
server._proxy.queue_notification("textDocument/didOpen", serialized)
|
|
397
483
|
if not server._proxy._lazy_start_fired:
|
|
398
484
|
# First file: kick off lazy start in background.
|
|
@@ -402,7 +488,10 @@ async def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
|
|
402
488
|
task.add_done_callback(_bg_tasks.discard)
|
|
403
489
|
|
|
404
490
|
# Custom diagnostics always publish immediately — never blocked by jdtls.
|
|
405
|
-
|
|
491
|
+
try:
|
|
492
|
+
_analyze_and_publish(uri)
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logger.error("Analysis failed on open for %s: %s", uri, e)
|
|
406
495
|
|
|
407
496
|
|
|
408
497
|
@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
|
|
@@ -420,7 +509,10 @@ async def on_did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
|
|
420
509
|
async def on_did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
|
421
510
|
"""Forward to jdtls and re-analyze immediately (no debounce on save)."""
|
|
422
511
|
_forward_or_queue("textDocument/didSave", _serialize_params(params))
|
|
423
|
-
|
|
512
|
+
try:
|
|
513
|
+
_analyze_and_publish(params.text_document.uri)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
logger.error("Analysis failed on save for %s: %s", params.text_document.uri, e)
|
|
424
516
|
|
|
425
517
|
|
|
426
518
|
@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
|
@@ -447,7 +539,10 @@ async def _lazy_start_jdtls(file_uri: str) -> None:
|
|
|
447
539
|
started = await server._proxy.ensure_started(server._init_params, file_uri, config=server._config)
|
|
448
540
|
if started:
|
|
449
541
|
logger.info("jdtls proxy active — full Java language support enabled")
|
|
450
|
-
|
|
542
|
+
if server._skip_jdtls_registration:
|
|
543
|
+
logger.info("Skipping dynamic capability registration (no-register mode)")
|
|
544
|
+
else:
|
|
545
|
+
await _register_jdtls_capabilities()
|
|
451
546
|
await server._proxy.flush_queued_notifications()
|
|
452
547
|
except Exception:
|
|
453
548
|
logger.warning("jdtls lazy start failed", exc_info=True)
|
|
@@ -647,55 +647,201 @@ class TestServerInternals:
|
|
|
647
647
|
# --------------------------------------------------------------------------
|
|
648
648
|
|
|
649
649
|
|
|
650
|
-
class
|
|
651
|
-
"""Tests for
|
|
650
|
+
class TestUserSuppressPatterns:
|
|
651
|
+
"""Tests for configurable suppressJdtlsPatterns."""
|
|
652
652
|
|
|
653
|
-
def
|
|
654
|
-
from java_functional_lsp.server import
|
|
653
|
+
def test_user_pattern_matches(self) -> None:
|
|
654
|
+
from java_functional_lsp.server import _compile_user_patterns, _is_jdtls_suppressed
|
|
655
655
|
|
|
656
|
-
|
|
657
|
-
|
|
656
|
+
patterns = _compile_user_patterns({"suppressJdtlsPatterns": [r"The method generateReport\(\) is undefined"]})
|
|
657
|
+
diag = {"message": "The method generateReport() is undefined for the type InternalService"}
|
|
658
|
+
assert _is_jdtls_suppressed(diag, patterns) is True
|
|
658
659
|
|
|
659
|
-
def
|
|
660
|
-
from java_functional_lsp.server import
|
|
660
|
+
def test_user_pattern_does_not_match_unrelated(self) -> None:
|
|
661
|
+
from java_functional_lsp.server import _compile_user_patterns, _is_jdtls_suppressed
|
|
661
662
|
|
|
662
|
-
|
|
663
|
-
|
|
663
|
+
patterns = _compile_user_patterns({"suppressJdtlsPatterns": [r"The method generateReport\(\) is undefined"]})
|
|
664
|
+
diag = {"message": "The method foo() is undefined for the type String"}
|
|
665
|
+
assert _is_jdtls_suppressed(diag, patterns) is False
|
|
664
666
|
|
|
665
|
-
def
|
|
666
|
-
|
|
667
|
+
def test_no_patterns_passes_everything(self) -> None:
|
|
668
|
+
"""With no user patterns, nothing is suppressed."""
|
|
669
|
+
from java_functional_lsp.server import _is_jdtls_suppressed
|
|
667
670
|
|
|
668
|
-
diag = {"
|
|
669
|
-
assert
|
|
671
|
+
diag = {"message": "The method builder() is undefined for the type Foo"}
|
|
672
|
+
assert _is_jdtls_suppressed(diag, []) is False
|
|
670
673
|
|
|
671
|
-
def
|
|
672
|
-
from java_functional_lsp.server import
|
|
674
|
+
def test_invalid_regex_skipped(self) -> None:
|
|
675
|
+
from java_functional_lsp.server import _compile_user_patterns
|
|
673
676
|
|
|
674
|
-
|
|
675
|
-
assert
|
|
677
|
+
patterns = _compile_user_patterns({"suppressJdtlsPatterns": [r"valid", r"[invalid"]})
|
|
678
|
+
assert len(patterns) == 1
|
|
679
|
+
assert patterns[0].search("valid") is not None
|
|
676
680
|
|
|
677
|
-
def
|
|
678
|
-
from java_functional_lsp.server import
|
|
681
|
+
def test_non_list_ignored(self) -> None:
|
|
682
|
+
from java_functional_lsp.server import _compile_user_patterns
|
|
679
683
|
|
|
680
|
-
|
|
681
|
-
assert
|
|
684
|
+
patterns = _compile_user_patterns({"suppressJdtlsPatterns": "not a list"})
|
|
685
|
+
assert patterns == []
|
|
682
686
|
|
|
683
|
-
def
|
|
684
|
-
from java_functional_lsp.server import
|
|
687
|
+
def test_empty_config(self) -> None:
|
|
688
|
+
from java_functional_lsp.server import _compile_user_patterns
|
|
685
689
|
|
|
686
|
-
|
|
687
|
-
assert
|
|
690
|
+
patterns = _compile_user_patterns({})
|
|
691
|
+
assert patterns == []
|
|
688
692
|
|
|
689
|
-
def test_missing_fields(self) -> None:
|
|
690
|
-
from java_functional_lsp.server import _is_lombok_false_positive
|
|
691
693
|
|
|
692
|
-
|
|
694
|
+
class TestJdtlsIsolation:
|
|
695
|
+
"""Tests that jdtls failures don't suppress custom diagnostics."""
|
|
693
696
|
|
|
694
|
-
def
|
|
695
|
-
|
|
697
|
+
def test_jdtls_exception_does_not_suppress_custom_diags(self) -> None:
|
|
698
|
+
"""If jdtls diagnostic processing throws, custom diagnostics still publish."""
|
|
699
|
+
from unittest.mock import patch
|
|
700
|
+
|
|
701
|
+
from java_functional_lsp.server import _run_analysis
|
|
702
|
+
|
|
703
|
+
java_source = "public class Foo { public String bar() { return null; } }"
|
|
704
|
+
|
|
705
|
+
with patch("java_functional_lsp.server.server") as mock_server:
|
|
706
|
+
mock_server._proxy.is_available = True
|
|
707
|
+
mock_server._proxy.get_cached_diagnostics.side_effect = RuntimeError("jdtls corrupt")
|
|
708
|
+
mock_server._user_suppress_patterns = []
|
|
709
|
+
mock_server._config = {}
|
|
710
|
+
mock_server._parser = __import__("java_functional_lsp.analyzers.base", fromlist=["get_parser"]).get_parser()
|
|
711
|
+
|
|
712
|
+
result = _run_analysis(java_source, "file:///test/Foo.java")
|
|
713
|
+
|
|
714
|
+
custom_diags = [d for d in result if d.source == "java-functional-lsp"]
|
|
715
|
+
assert len(custom_diags) > 0, "Custom diagnostics should publish even when jdtls fails"
|
|
716
|
+
|
|
717
|
+
def test_jdtls_unavailable_still_publishes_custom_diags(self) -> None:
|
|
718
|
+
"""When jdtls is not available, custom diagnostics still publish."""
|
|
719
|
+
from unittest.mock import patch
|
|
720
|
+
|
|
721
|
+
from java_functional_lsp.server import _run_analysis
|
|
722
|
+
|
|
723
|
+
java_source = "public class Foo { public String bar() { return null; } }"
|
|
724
|
+
|
|
725
|
+
with patch("java_functional_lsp.server.server") as mock_server:
|
|
726
|
+
mock_server._proxy.is_available = False
|
|
727
|
+
mock_server._config = {}
|
|
728
|
+
mock_server._parser = __import__("java_functional_lsp.analyzers.base", fromlist=["get_parser"]).get_parser()
|
|
729
|
+
|
|
730
|
+
result = _run_analysis(java_source, "file:///test/Foo.java")
|
|
731
|
+
|
|
732
|
+
custom_diags = [d for d in result if d.source == "java-functional-lsp"]
|
|
733
|
+
assert len(custom_diags) > 0, "Custom diagnostics should publish without jdtls"
|
|
734
|
+
|
|
735
|
+
def test_analyze_and_publish_exception_does_not_crash_handler(self) -> None:
|
|
736
|
+
"""Handler-level try/except prevents server crash when _analyze_and_publish raises."""
|
|
737
|
+
from unittest.mock import patch
|
|
738
|
+
|
|
739
|
+
from java_functional_lsp.server import _deferred_validate
|
|
740
|
+
|
|
741
|
+
with patch("java_functional_lsp.server._analyze_and_publish", side_effect=RuntimeError("boom")):
|
|
742
|
+
import asyncio
|
|
743
|
+
|
|
744
|
+
loop = asyncio.new_event_loop()
|
|
745
|
+
try:
|
|
746
|
+
# _deferred_validate should catch the exception and log, not raise
|
|
747
|
+
loop.run_until_complete(_deferred_validate("file:///test/Foo.java"))
|
|
748
|
+
finally:
|
|
749
|
+
loop.close()
|
|
750
|
+
# If we get here without exception, the handler caught it correctly
|
|
751
|
+
|
|
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
|
|
696
796
|
|
|
697
|
-
|
|
698
|
-
|
|
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"
|
|
699
845
|
|
|
700
846
|
|
|
701
847
|
class TestFindLombokJar:
|
|
@@ -761,6 +907,51 @@ class TestFindLombokJar:
|
|
|
761
907
|
assert any("does not exist" in r.getMessage() for r in caplog.records)
|
|
762
908
|
|
|
763
909
|
|
|
910
|
+
class TestCacheClear:
|
|
911
|
+
"""Tests for _clear_cache_on_version_change()."""
|
|
912
|
+
|
|
913
|
+
def test_clears_on_version_mismatch(self, tmp_path: Any) -> None:
|
|
914
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
915
|
+
|
|
916
|
+
cache = tmp_path / "jdtls-data"
|
|
917
|
+
cache.mkdir()
|
|
918
|
+
stale_dir = cache / "abc123"
|
|
919
|
+
stale_dir.mkdir()
|
|
920
|
+
(cache / ".version").write_text("0.7.9")
|
|
921
|
+
|
|
922
|
+
_clear_cache_on_version_change(cache)
|
|
923
|
+
|
|
924
|
+
assert not stale_dir.exists(), "Stale data-dir should be cleared"
|
|
925
|
+
assert cache.exists(), "Cache root should be recreated"
|
|
926
|
+
from java_functional_lsp import __version__
|
|
927
|
+
|
|
928
|
+
assert (cache / ".version").read_text() == __version__
|
|
929
|
+
|
|
930
|
+
def test_keeps_cache_on_same_version(self, tmp_path: Any) -> None:
|
|
931
|
+
from java_functional_lsp import __version__
|
|
932
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
933
|
+
|
|
934
|
+
cache = tmp_path / "jdtls-data"
|
|
935
|
+
cache.mkdir()
|
|
936
|
+
existing_dir = cache / "abc123"
|
|
937
|
+
existing_dir.mkdir()
|
|
938
|
+
(cache / ".version").write_text(__version__)
|
|
939
|
+
|
|
940
|
+
_clear_cache_on_version_change(cache)
|
|
941
|
+
|
|
942
|
+
assert existing_dir.exists(), "Existing data-dir should be preserved"
|
|
943
|
+
|
|
944
|
+
def test_creates_marker_on_first_run(self, tmp_path: Any) -> None:
|
|
945
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
946
|
+
|
|
947
|
+
cache = tmp_path / "jdtls-data"
|
|
948
|
+
_clear_cache_on_version_change(cache)
|
|
949
|
+
|
|
950
|
+
from java_functional_lsp import __version__
|
|
951
|
+
|
|
952
|
+
assert (cache / ".version").read_text() == __version__
|
|
953
|
+
|
|
954
|
+
|
|
764
955
|
# Subprocess-based tests — zero mocks, real LSP transport
|
|
765
956
|
# --------------------------------------------------------------------------
|
|
766
957
|
|
|
@@ -184,7 +184,7 @@ wheels = [
|
|
|
184
184
|
|
|
185
185
|
[[package]]
|
|
186
186
|
name = "java-functional-lsp"
|
|
187
|
-
version = "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.
|
|
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/
|
|
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/
|
|
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]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/ISSUE_TEMPLATE/bug-report.md
RENAMED
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/ISSUE_TEMPLATE/feature-request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/release-drafter.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/.github/workflows/update-homebrew.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/editors/intellij/lsp4ij-template.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.1}/src/java_functional_lsp/analyzers/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|