java-functional-lsp 0.9.10__tar.gz → 0.9.12__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.9.10 → java_functional_lsp-0.9.12}/PKG-INFO +32 -2
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/README.md +31 -1
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/pyproject.toml +1 -1
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/proxy.py +92 -27
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/server.py +19 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_proxy.py +52 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_server.py +78 -13
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.gitignore +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/LICENSE +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/SKILL.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/analyzers/base.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/fixes.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/merkle.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/__init__.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/conftest.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_base.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_config.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_e2e_jdtls.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_functional_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_merkle.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_merkle_proxy.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_mutation_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/tests/test_suppress.py +0 -0
- {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-functional-lsp
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.12
|
|
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
|
|
@@ -270,7 +270,37 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
270
270
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
271
271
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
272
272
|
|
|
273
|
-
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
273
|
+
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
274
|
+
|
|
275
|
+
### jdtls settings
|
|
276
|
+
|
|
277
|
+
The server sends Maven/Gradle settings to jdtls at startup via `initializationOptions.settings`. Defaults are optimized for Maven monorepos (Maven enabled, Gradle disabled, build artifact exclusions). Override via `.java-functional-lsp.json`:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"jdtls": {
|
|
282
|
+
"settings": {
|
|
283
|
+
"java": {
|
|
284
|
+
"import": {
|
|
285
|
+
"maven": { "enabled": true },
|
|
286
|
+
"gradle": { "enabled": true },
|
|
287
|
+
"exclusions": ["**/node_modules/**", "**/target/**"]
|
|
288
|
+
},
|
|
289
|
+
"configuration": { "updateBuildConfiguration": "automatic" },
|
|
290
|
+
"maven": { "downloadSources": true }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Custom settings fully replace the defaults (no merge). See the [jdtls Preferences reference](https://github.com/eclipse-jdtls/eclipse.jdt.ls/blob/main/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java) for all available keys.
|
|
298
|
+
|
|
299
|
+
### jdtls cache
|
|
300
|
+
|
|
301
|
+
The jdtls Eclipse workspace index is cached in `~/.cache/jdtls-data/`. Warm starts (~10-20s) reuse this cache; cold starts (60-120s) rebuild from scratch. The cache is automatically invalidated when jdtls or Java is upgraded, but **not** when java-functional-lsp is upgraded — our Python code changes don't affect the Eclipse index.
|
|
302
|
+
|
|
303
|
+
To force a clean rebuild: `rm -rf ~/.cache/jdtls-data/`
|
|
274
304
|
|
|
275
305
|
### Suppressing jdtls diagnostics
|
|
276
306
|
|
|
@@ -242,7 +242,37 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
242
242
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
243
243
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
244
244
|
|
|
245
|
-
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
245
|
+
If Lombok is used in your project but the jar isn't found, the server logs a warning.
|
|
246
|
+
|
|
247
|
+
### jdtls settings
|
|
248
|
+
|
|
249
|
+
The server sends Maven/Gradle settings to jdtls at startup via `initializationOptions.settings`. Defaults are optimized for Maven monorepos (Maven enabled, Gradle disabled, build artifact exclusions). Override via `.java-functional-lsp.json`:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"jdtls": {
|
|
254
|
+
"settings": {
|
|
255
|
+
"java": {
|
|
256
|
+
"import": {
|
|
257
|
+
"maven": { "enabled": true },
|
|
258
|
+
"gradle": { "enabled": true },
|
|
259
|
+
"exclusions": ["**/node_modules/**", "**/target/**"]
|
|
260
|
+
},
|
|
261
|
+
"configuration": { "updateBuildConfiguration": "automatic" },
|
|
262
|
+
"maven": { "downloadSources": true }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Custom settings fully replace the defaults (no merge). See the [jdtls Preferences reference](https://github.com/eclipse-jdtls/eclipse.jdt.ls/blob/main/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java) for all available keys.
|
|
270
|
+
|
|
271
|
+
### jdtls cache
|
|
272
|
+
|
|
273
|
+
The jdtls Eclipse workspace index is cached in `~/.cache/jdtls-data/`. Warm starts (~10-20s) reuse this cache; cold starts (60-120s) rebuild from scratch. The cache is automatically invalidated when jdtls or Java is upgraded, but **not** when java-functional-lsp is upgraded — our Python code changes don't affect the Eclipse index.
|
|
274
|
+
|
|
275
|
+
To force a clean rebuild: `rm -rf ~/.cache/jdtls-data/`
|
|
246
276
|
|
|
247
277
|
### Suppressing jdtls diagnostics
|
|
248
278
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "java-functional-lsp"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.12"
|
|
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" }
|
|
@@ -30,6 +30,27 @@ DEFAULT_JVM_MAX_HEAP = "4g"
|
|
|
30
30
|
_STDERR_LINE_MAX = 1000
|
|
31
31
|
_MAX_EXPANDED_GROUPS: int = 5 # hard cap on concurrent Maven group workspace folders (prevents jdtls OOM)
|
|
32
32
|
|
|
33
|
+
# Default jdtls initialization settings. Sent via initializationOptions.settings
|
|
34
|
+
# so they apply BEFORE the Maven import scan (didChangeConfiguration is too late).
|
|
35
|
+
# Users can override via .java-functional-lsp.json: {"jdtls": {"settings": {...}}}
|
|
36
|
+
_DEFAULT_JDTLS_SETTINGS: dict[str, Any] = {
|
|
37
|
+
"java": {
|
|
38
|
+
"import": {
|
|
39
|
+
"maven": {"enabled": True},
|
|
40
|
+
"gradle": {"enabled": False},
|
|
41
|
+
"exclusions": [
|
|
42
|
+
"**/node_modules/**",
|
|
43
|
+
"**/.metadata/**",
|
|
44
|
+
"**/archetype-resources/**",
|
|
45
|
+
"**/META-INF/maven/**",
|
|
46
|
+
"**/target/**",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
"configuration": {"updateBuildConfiguration": "automatic"},
|
|
50
|
+
"maven": {"downloadSources": False},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
#: Minimum Java major version required by jdtls 1.57+. Anything below this
|
|
34
55
|
#: is rejected by the jdtls Python launcher with ``"jdtls requires at least
|
|
35
56
|
#: Java 21"`` before the server even starts.
|
|
@@ -558,25 +579,43 @@ def _wipe_data_dir(data_dir: Path) -> None:
|
|
|
558
579
|
logger.info("jdtls: wiped data-dir %s after init timeout", _redact_path(str(data_dir)))
|
|
559
580
|
|
|
560
581
|
|
|
561
|
-
def
|
|
562
|
-
"""
|
|
582
|
+
def _compute_cache_marker() -> str:
|
|
583
|
+
"""Compute a stable cache marker from jdtls version path + Java version.
|
|
563
584
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
585
|
+
Our Python package version changes (pip upgrades) do NOT affect the jdtls
|
|
586
|
+
Eclipse workspace index — only jdtls binary upgrades or Java version
|
|
587
|
+
changes do. Resolves through symlinks so Homebrew Cellar version changes
|
|
588
|
+
(e.g. ``/opt/homebrew/Cellar/jdtls/1.58.0/...``) are detected even when
|
|
589
|
+
the wrapper script at ``/opt/homebrew/bin/jdtls`` doesn't change.
|
|
567
590
|
"""
|
|
568
|
-
|
|
591
|
+
parts: list[str] = []
|
|
592
|
+
jdtls_path = shutil.which("jdtls")
|
|
593
|
+
if jdtls_path:
|
|
594
|
+
resolved = str(Path(jdtls_path).resolve())
|
|
595
|
+
parts.append(hashlib.sha256(resolved.encode()).hexdigest()[:12])
|
|
596
|
+
parts.append(str(_MIN_JDTLS_JAVA_MAJOR))
|
|
597
|
+
return "|".join(parts)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _clear_cache_on_version_change(cache_root: Path) -> None:
|
|
601
|
+
"""Clear jdtls data cache when the jdtls installation changes.
|
|
569
602
|
|
|
603
|
+
Stores a ``.version`` marker in the cache root. The marker is derived
|
|
604
|
+
from the resolved jdtls binary path + Java version requirement — NOT
|
|
605
|
+
from our Python ``__version__``. This preserves warm caches across
|
|
606
|
+
java-functional-lsp upgrades (which don't affect the Eclipse index).
|
|
607
|
+
"""
|
|
608
|
+
current_marker = _compute_cache_marker()
|
|
570
609
|
marker = cache_root / ".version"
|
|
571
610
|
try:
|
|
572
|
-
if marker.exists() and marker.read_text().strip() ==
|
|
611
|
+
if marker.exists() and marker.read_text().strip() == current_marker:
|
|
573
612
|
return
|
|
574
|
-
#
|
|
613
|
+
# jdtls or Java version changed, or first run — clear stale caches.
|
|
575
614
|
if cache_root.exists():
|
|
576
615
|
shutil.rmtree(cache_root)
|
|
577
|
-
logger.info("jdtls: cleared stale cache (
|
|
616
|
+
logger.info("jdtls: cleared stale cache (marker changed to %s)", current_marker)
|
|
578
617
|
cache_root.mkdir(parents=True, exist_ok=True)
|
|
579
|
-
marker.write_text(
|
|
618
|
+
marker.write_text(current_marker)
|
|
580
619
|
except OSError as e:
|
|
581
620
|
logger.warning("jdtls: failed to manage cache version marker: %s", e)
|
|
582
621
|
|
|
@@ -622,6 +661,48 @@ def _find_lombok_jar(config: Mapping[str, Any] | None = None) -> str | None:
|
|
|
622
661
|
return None
|
|
623
662
|
|
|
624
663
|
|
|
664
|
+
def _build_effective_params(
|
|
665
|
+
init_params: dict[str, Any],
|
|
666
|
+
module_root_uri: str | None,
|
|
667
|
+
original_root: str,
|
|
668
|
+
config: Mapping[str, Any] | None,
|
|
669
|
+
) -> dict[str, Any]:
|
|
670
|
+
"""Build the ``initialize`` request params for jdtls.
|
|
671
|
+
|
|
672
|
+
Deep-copies *init_params*, scopes to *module_root_uri* if provided,
|
|
673
|
+
injects workspaceFolders capability, and injects Maven/Gradle settings
|
|
674
|
+
from *config* (or defaults) into ``initializationOptions.settings``.
|
|
675
|
+
"""
|
|
676
|
+
from pygls.uris import to_fs_path
|
|
677
|
+
|
|
678
|
+
effective_params = copy.deepcopy(init_params)
|
|
679
|
+
if module_root_uri:
|
|
680
|
+
effective_params["rootUri"] = module_root_uri
|
|
681
|
+
effective_params["rootPath"] = to_fs_path(module_root_uri)
|
|
682
|
+
logger.info(
|
|
683
|
+
"jdtls: scoping to module %s (full root: %s)",
|
|
684
|
+
_redact_path(module_root_uri),
|
|
685
|
+
_redact_path(original_root),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
# Inject workspaceFolders capability for later expansion.
|
|
689
|
+
caps = effective_params.setdefault("capabilities", {})
|
|
690
|
+
ws = caps.setdefault("workspace", {})
|
|
691
|
+
ws["workspaceFolders"] = True
|
|
692
|
+
|
|
693
|
+
# Inject jdtls Maven/Gradle settings into initializationOptions.
|
|
694
|
+
# Must be in initializationOptions (not didChangeConfiguration) so they
|
|
695
|
+
# apply before the Maven import scan starts.
|
|
696
|
+
jdtls_settings = copy.deepcopy(_DEFAULT_JDTLS_SETTINGS)
|
|
697
|
+
if config and "jdtls" in config and "settings" in config["jdtls"]:
|
|
698
|
+
jdtls_settings = copy.deepcopy(config["jdtls"]["settings"])
|
|
699
|
+
init_opts = effective_params.setdefault("initializationOptions", {})
|
|
700
|
+
init_opts["settings"] = jdtls_settings
|
|
701
|
+
logger.debug("jdtls: initializationOptions.settings = %s", jdtls_settings)
|
|
702
|
+
|
|
703
|
+
return effective_params
|
|
704
|
+
|
|
705
|
+
|
|
625
706
|
class JdtlsProxy:
|
|
626
707
|
"""Manages a jdtls subprocess and provides async request/notification forwarding."""
|
|
627
708
|
|
|
@@ -724,23 +805,7 @@ class JdtlsProxy:
|
|
|
724
805
|
data_dir = cache_root / data_hash
|
|
725
806
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
726
807
|
|
|
727
|
-
|
|
728
|
-
effective_params = copy.deepcopy(init_params)
|
|
729
|
-
if module_root_uri:
|
|
730
|
-
effective_params["rootUri"] = module_root_uri
|
|
731
|
-
from pygls.uris import to_fs_path
|
|
732
|
-
|
|
733
|
-
effective_params["rootPath"] = to_fs_path(module_root_uri)
|
|
734
|
-
logger.info(
|
|
735
|
-
"jdtls: scoping to module %s (full root: %s)",
|
|
736
|
-
_redact_path(module_root_uri),
|
|
737
|
-
_redact_path(original_root),
|
|
738
|
-
)
|
|
739
|
-
|
|
740
|
-
# Inject workspaceFolders capability for later expansion.
|
|
741
|
-
caps = effective_params.setdefault("capabilities", {})
|
|
742
|
-
ws = caps.setdefault("workspace", {})
|
|
743
|
-
ws["workspaceFolders"] = True
|
|
808
|
+
effective_params = _build_effective_params(init_params, module_root_uri, original_root, config)
|
|
744
809
|
|
|
745
810
|
jdtls_cmd: list[str] = [
|
|
746
811
|
jdtls_path,
|
|
@@ -12,6 +12,7 @@ import logging
|
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
14
|
import sys
|
|
15
|
+
import time
|
|
15
16
|
from collections.abc import Coroutine
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any
|
|
@@ -635,6 +636,24 @@ async def on_did_close(params: lsp.DidCloseTextDocumentParams) -> None:
|
|
|
635
636
|
_forward_or_queue("textDocument/didClose", _serialize_params(params))
|
|
636
637
|
|
|
637
638
|
|
|
639
|
+
_DIDCHANGE_CONFIG_COOLDOWN = 30.0 # seconds — rate-limit didChangeConfiguration forwarding
|
|
640
|
+
_last_config_forward: float = 0.0
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@server.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
|
|
644
|
+
def on_did_change_configuration(params: lsp.DidChangeConfigurationParams) -> None:
|
|
645
|
+
"""Forward IDE configuration changes to jdtls (rate-limited)."""
|
|
646
|
+
global _last_config_forward
|
|
647
|
+
if not server._proxy.is_available:
|
|
648
|
+
return
|
|
649
|
+
now = time.monotonic()
|
|
650
|
+
if now - _last_config_forward < _DIDCHANGE_CONFIG_COOLDOWN:
|
|
651
|
+
logger.debug("jdtls: throttling didChangeConfiguration (%.0fs cooldown)", _DIDCHANGE_CONFIG_COOLDOWN)
|
|
652
|
+
return
|
|
653
|
+
_last_config_forward = now
|
|
654
|
+
_fire_and_forget(server._proxy.send_notification("workspace/didChangeConfiguration", _serialize_params(params)))
|
|
655
|
+
|
|
656
|
+
|
|
638
657
|
async def _lazy_start_jdtls(file_uri: str) -> None:
|
|
639
658
|
"""Background task: start jdtls scoped to the module containing *file_uri*.
|
|
640
659
|
|
|
@@ -1417,6 +1417,58 @@ class TestLazyStart:
|
|
|
1417
1417
|
# ---------------------------------------------------------------------------
|
|
1418
1418
|
|
|
1419
1419
|
|
|
1420
|
+
class TestBuildEffectiveParams:
|
|
1421
|
+
"""Tests for _build_effective_params."""
|
|
1422
|
+
|
|
1423
|
+
def test_defaults_injected_when_no_config(self) -> None:
|
|
1424
|
+
from java_functional_lsp.proxy import _build_effective_params
|
|
1425
|
+
|
|
1426
|
+
result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", None)
|
|
1427
|
+
settings = result["initializationOptions"]["settings"]
|
|
1428
|
+
assert settings["java"]["import"]["maven"]["enabled"] is True
|
|
1429
|
+
assert settings["java"]["import"]["gradle"]["enabled"] is False
|
|
1430
|
+
assert "**/target/**" in settings["java"]["import"]["exclusions"]
|
|
1431
|
+
|
|
1432
|
+
def test_config_override_replaces_defaults(self) -> None:
|
|
1433
|
+
from java_functional_lsp.proxy import _build_effective_params
|
|
1434
|
+
|
|
1435
|
+
custom = {"java": {"custom": "value"}}
|
|
1436
|
+
config = {"jdtls": {"settings": custom}}
|
|
1437
|
+
result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", config)
|
|
1438
|
+
settings = result["initializationOptions"]["settings"]
|
|
1439
|
+
assert settings == {"java": {"custom": "value"}}
|
|
1440
|
+
assert "import" not in settings.get("java", {})
|
|
1441
|
+
|
|
1442
|
+
def test_does_not_mutate_original_init_params(self) -> None:
|
|
1443
|
+
from java_functional_lsp.proxy import _build_effective_params
|
|
1444
|
+
|
|
1445
|
+
original = {"rootUri": "file:///ws", "capabilities": {}}
|
|
1446
|
+
_build_effective_params(original, "file:///mod", "file:///ws", None)
|
|
1447
|
+
assert "initializationOptions" not in original
|
|
1448
|
+
assert original["rootUri"] == "file:///ws"
|
|
1449
|
+
|
|
1450
|
+
def test_module_root_uri_scopes_root(self) -> None:
|
|
1451
|
+
from java_functional_lsp.proxy import _build_effective_params
|
|
1452
|
+
|
|
1453
|
+
result = _build_effective_params({"rootUri": "file:///ws"}, "file:///mod", "file:///ws", None)
|
|
1454
|
+
assert result["rootUri"] == "file:///mod"
|
|
1455
|
+
|
|
1456
|
+
def test_workspace_folders_capability_always_set(self) -> None:
|
|
1457
|
+
from java_functional_lsp.proxy import _build_effective_params
|
|
1458
|
+
|
|
1459
|
+
result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", None)
|
|
1460
|
+
assert result["capabilities"]["workspace"]["workspaceFolders"] is True
|
|
1461
|
+
|
|
1462
|
+
def test_deepcopy_prevents_default_mutation(self) -> None:
|
|
1463
|
+
from java_functional_lsp.proxy import _DEFAULT_JDTLS_SETTINGS, _build_effective_params
|
|
1464
|
+
|
|
1465
|
+
result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", None)
|
|
1466
|
+
# Mutate the returned settings
|
|
1467
|
+
result["initializationOptions"]["settings"]["java"]["import"]["exclusions"].append("MUTATED")
|
|
1468
|
+
# Module-level defaults must be unchanged
|
|
1469
|
+
assert "MUTATED" not in _DEFAULT_JDTLS_SETTINGS["java"]["import"]["exclusions"]
|
|
1470
|
+
|
|
1471
|
+
|
|
1420
1472
|
class TestWipeDataDir:
|
|
1421
1473
|
def test_wipe_removes_directory(self, tmp_path: Path) -> None:
|
|
1422
1474
|
"""Directory is removed and info is logged."""
|
|
@@ -1547,49 +1547,114 @@ class TestFindLombokJar:
|
|
|
1547
1547
|
assert any("does not exist" in r.getMessage() for r in caplog.records)
|
|
1548
1548
|
|
|
1549
1549
|
|
|
1550
|
+
class TestDidChangeConfiguration:
|
|
1551
|
+
"""Tests for workspace/didChangeConfiguration forwarding."""
|
|
1552
|
+
|
|
1553
|
+
def test_forwards_to_jdtls_when_available(self) -> None:
|
|
1554
|
+
from unittest.mock import AsyncMock, patch
|
|
1555
|
+
|
|
1556
|
+
import java_functional_lsp.server as srv_mod
|
|
1557
|
+
from java_functional_lsp.server import on_did_change_configuration, server
|
|
1558
|
+
|
|
1559
|
+
srv_mod._last_config_forward = 0.0 # reset cooldown
|
|
1560
|
+
server._proxy._available = True
|
|
1561
|
+
server._proxy.send_notification = AsyncMock() # type: ignore[assignment]
|
|
1562
|
+
params = lsp.DidChangeConfigurationParams(settings={"java": {"autobuild": {"enabled": True}}})
|
|
1563
|
+
with patch("java_functional_lsp.server._fire_and_forget") as mock_faf:
|
|
1564
|
+
on_did_change_configuration(params)
|
|
1565
|
+
mock_faf.assert_called_once()
|
|
1566
|
+
|
|
1567
|
+
def test_throttled_within_cooldown(self) -> None:
|
|
1568
|
+
from unittest.mock import patch
|
|
1569
|
+
|
|
1570
|
+
import java_functional_lsp.server as srv_mod
|
|
1571
|
+
from java_functional_lsp.server import on_did_change_configuration, server
|
|
1572
|
+
|
|
1573
|
+
server._proxy._available = True
|
|
1574
|
+
import time
|
|
1575
|
+
|
|
1576
|
+
srv_mod._last_config_forward = time.monotonic() # just sent
|
|
1577
|
+
params = lsp.DidChangeConfigurationParams(settings={})
|
|
1578
|
+
with patch("java_functional_lsp.server._fire_and_forget") as mock_faf:
|
|
1579
|
+
on_did_change_configuration(params)
|
|
1580
|
+
mock_faf.assert_not_called()
|
|
1581
|
+
|
|
1582
|
+
def test_skipped_when_proxy_unavailable(self) -> None:
|
|
1583
|
+
from unittest.mock import patch
|
|
1584
|
+
|
|
1585
|
+
from java_functional_lsp.server import on_did_change_configuration, server
|
|
1586
|
+
|
|
1587
|
+
server._proxy._available = False
|
|
1588
|
+
params = lsp.DidChangeConfigurationParams(settings={})
|
|
1589
|
+
with patch("java_functional_lsp.server._fire_and_forget") as mock_faf:
|
|
1590
|
+
on_did_change_configuration(params)
|
|
1591
|
+
mock_faf.assert_not_called()
|
|
1592
|
+
|
|
1593
|
+
|
|
1550
1594
|
class TestCacheClear:
|
|
1551
|
-
"""Tests for _clear_cache_on_version_change()."""
|
|
1595
|
+
"""Tests for _clear_cache_on_version_change() and _compute_cache_marker()."""
|
|
1552
1596
|
|
|
1553
1597
|
def test_clears_on_version_mismatch(self, tmp_path: Any) -> None:
|
|
1554
|
-
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
1598
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change, _compute_cache_marker
|
|
1555
1599
|
|
|
1556
1600
|
cache = tmp_path / "jdtls-data"
|
|
1557
1601
|
cache.mkdir()
|
|
1558
1602
|
stale_dir = cache / "abc123"
|
|
1559
1603
|
stale_dir.mkdir()
|
|
1560
|
-
(cache / ".version").write_text("
|
|
1604
|
+
(cache / ".version").write_text("old-stale-marker")
|
|
1561
1605
|
|
|
1562
1606
|
_clear_cache_on_version_change(cache)
|
|
1563
1607
|
|
|
1564
1608
|
assert not stale_dir.exists(), "Stale data-dir should be cleared"
|
|
1565
1609
|
assert cache.exists(), "Cache root should be recreated"
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
assert (cache / ".version").read_text() == __version__
|
|
1610
|
+
assert (cache / ".version").read_text() == _compute_cache_marker()
|
|
1569
1611
|
|
|
1570
|
-
def
|
|
1571
|
-
from java_functional_lsp import
|
|
1572
|
-
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
1612
|
+
def test_keeps_cache_on_same_marker(self, tmp_path: Any) -> None:
|
|
1613
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change, _compute_cache_marker
|
|
1573
1614
|
|
|
1574
1615
|
cache = tmp_path / "jdtls-data"
|
|
1575
1616
|
cache.mkdir()
|
|
1576
1617
|
existing_dir = cache / "abc123"
|
|
1577
1618
|
existing_dir.mkdir()
|
|
1578
|
-
(cache / ".version").write_text(
|
|
1619
|
+
(cache / ".version").write_text(_compute_cache_marker())
|
|
1579
1620
|
|
|
1580
1621
|
_clear_cache_on_version_change(cache)
|
|
1581
1622
|
|
|
1582
1623
|
assert existing_dir.exists(), "Existing data-dir should be preserved"
|
|
1583
1624
|
|
|
1584
1625
|
def test_creates_marker_on_first_run(self, tmp_path: Any) -> None:
|
|
1585
|
-
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
1626
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change, _compute_cache_marker
|
|
1586
1627
|
|
|
1587
1628
|
cache = tmp_path / "jdtls-data"
|
|
1588
1629
|
_clear_cache_on_version_change(cache)
|
|
1589
1630
|
|
|
1590
|
-
|
|
1631
|
+
assert (cache / ".version").read_text() == _compute_cache_marker()
|
|
1632
|
+
|
|
1633
|
+
def test_python_version_bump_preserves_cache(self) -> None:
|
|
1634
|
+
"""Our __version__ is NOT in the cache marker — pip upgrades don't wipe."""
|
|
1635
|
+
from unittest.mock import patch
|
|
1636
|
+
|
|
1637
|
+
from java_functional_lsp.proxy import _compute_cache_marker
|
|
1638
|
+
|
|
1639
|
+
marker_before = _compute_cache_marker()
|
|
1640
|
+
with patch("java_functional_lsp.__version__", "99.99.99"):
|
|
1641
|
+
marker_after = _compute_cache_marker()
|
|
1642
|
+
assert marker_before == marker_after, "Changing __version__ must not change the cache marker"
|
|
1643
|
+
|
|
1644
|
+
def test_different_jdtls_paths_produce_different_markers(self) -> None:
|
|
1645
|
+
"""Different resolved jdtls paths produce different cache markers."""
|
|
1646
|
+
from pathlib import Path
|
|
1647
|
+
from unittest.mock import patch
|
|
1648
|
+
|
|
1649
|
+
from java_functional_lsp.proxy import _compute_cache_marker
|
|
1591
1650
|
|
|
1592
|
-
|
|
1651
|
+
with patch("java_functional_lsp.proxy.shutil.which", return_value="/opt/jdtls/1.57/bin/jdtls"):
|
|
1652
|
+
with patch("java_functional_lsp.proxy.Path.resolve", return_value=Path("/opt/jdtls/1.57/bin/jdtls")):
|
|
1653
|
+
marker_a = _compute_cache_marker()
|
|
1654
|
+
with patch("java_functional_lsp.proxy.shutil.which", return_value="/opt/jdtls/1.58/bin/jdtls"):
|
|
1655
|
+
with patch("java_functional_lsp.proxy.Path.resolve", return_value=Path("/opt/jdtls/1.58/bin/jdtls")):
|
|
1656
|
+
marker_b = _compute_cache_marker()
|
|
1657
|
+
assert marker_a != marker_b
|
|
1593
1658
|
|
|
1594
1659
|
|
|
1595
1660
|
# Subprocess-based tests — zero mocks, real LSP transport
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.github/ISSUE_TEMPLATE/bug-report.md
RENAMED
|
File without changes
|
{java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.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.9.10 → java_functional_lsp-0.9.12}/.github/workflows/release-drafter.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/.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
|
|
File without changes
|
{java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/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
|
{java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/src/java_functional_lsp/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.9.10 → java_functional_lsp-0.9.12}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|