java-functional-lsp 0.7.9__tar.gz → 0.8.0__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.0}/PKG-INFO +18 -2
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/README.md +17 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/pyproject.toml +1 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/proxy.py +44 -16
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/server.py +50 -23
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_server.py +130 -33
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/uv.lock +1 -1
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.gitignore +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/LICENSE +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/SKILL.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/analyzers/base.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/src/java_functional_lsp/fixes.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/__init__.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/conftest.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_base.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_config.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_e2e_jdtls.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_functional_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_mutation_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_proxy.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/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.0
|
|
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
|
|
@@ -230,6 +230,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
230
230
|
**Options:**
|
|
231
231
|
- `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
|
|
232
232
|
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
233
|
+
- `suppressJdtlsPatterns` — list of regex patterns to suppress jdtls diagnostics (see below)
|
|
233
234
|
|
|
234
235
|
**Spring-aware behavior:**
|
|
235
236
|
- `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
|
|
@@ -267,7 +268,22 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
267
268
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
268
269
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
269
270
|
|
|
270
|
-
If Lombok is used in your project but the jar isn't found, the server
|
|
271
|
+
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`.
|
|
272
|
+
|
|
273
|
+
### Suppressing jdtls diagnostics
|
|
274
|
+
|
|
275
|
+
For project-specific jdtls false positives (e.g., annotation processor methods, MapStruct mappers), use `suppressJdtlsPatterns` to add custom regex filters:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"suppressJdtlsPatterns": [
|
|
280
|
+
"The method \\w+Mapper\\(\\) is undefined",
|
|
281
|
+
"cannot be resolved to a type"
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Each entry is a regex matched against jdtls diagnostic messages. Invalid patterns are skipped with a warning.
|
|
271
287
|
|
|
272
288
|
## Code actions (quick fixes)
|
|
273
289
|
|
|
@@ -202,6 +202,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
202
202
|
**Options:**
|
|
203
203
|
- `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
|
|
204
204
|
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
205
|
+
- `suppressJdtlsPatterns` — list of regex patterns to suppress jdtls diagnostics (see below)
|
|
205
206
|
|
|
206
207
|
**Spring-aware behavior:**
|
|
207
208
|
- `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
|
|
@@ -239,7 +240,22 @@ The server auto-discovers `lombok.jar` from these locations (first match wins):
|
|
|
239
240
|
3. **Maven cache** — auto-discovered from `~/.m2/repository/org/projectlombok/lombok/`
|
|
240
241
|
4. **Dedicated directory** — `~/.jdtls-libs/lombok.jar`
|
|
241
242
|
|
|
242
|
-
If Lombok is used in your project but the jar isn't found, the server
|
|
243
|
+
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`.
|
|
244
|
+
|
|
245
|
+
### Suppressing jdtls diagnostics
|
|
246
|
+
|
|
247
|
+
For project-specific jdtls false positives (e.g., annotation processor methods, MapStruct mappers), use `suppressJdtlsPatterns` to add custom regex filters:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"suppressJdtlsPatterns": [
|
|
252
|
+
"The method \\w+Mapper\\(\\) is undefined",
|
|
253
|
+
"cannot be resolved to a type"
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Each entry is a regex matched against jdtls diagnostic messages. Invalid patterns are skipped with a warning.
|
|
243
259
|
|
|
244
260
|
## Code actions (quick fixes)
|
|
245
261
|
|
|
@@ -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.0"
|
|
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",
|
|
@@ -65,6 +65,7 @@ class JavaFunctionalLspServer(LanguageServer):
|
|
|
65
65
|
self._config: dict[str, Any] = {}
|
|
66
66
|
self._init_params: dict[str, Any] = {}
|
|
67
67
|
self._proxy = JdtlsProxy(on_diagnostics=self._on_jdtls_diagnostics)
|
|
68
|
+
self._user_suppress_patterns: list[re.Pattern[str]] = []
|
|
68
69
|
|
|
69
70
|
def _on_jdtls_diagnostics(self, uri: str, diagnostics: list[Any]) -> None:
|
|
70
71
|
"""Called when jdtls publishes diagnostics — merge with custom and re-publish.
|
|
@@ -198,38 +199,54 @@ def _jdtls_raw_to_lsp_diagnostics(raw_diagnostics: list[Any]) -> list[lsp.Diagno
|
|
|
198
199
|
return result
|
|
199
200
|
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
202
|
+
def _compile_user_patterns(config: dict[str, Any]) -> list[re.Pattern[str]]:
|
|
203
|
+
"""Compile user-defined suppressJdtlsPatterns from config."""
|
|
204
|
+
raw = config.get("suppressJdtlsPatterns", [])
|
|
205
|
+
if not isinstance(raw, list):
|
|
206
|
+
logger.warning("suppressJdtlsPatterns must be a list of regex strings, got %s", type(raw).__name__)
|
|
207
|
+
return []
|
|
208
|
+
patterns: list[re.Pattern[str]] = []
|
|
209
|
+
for entry in raw:
|
|
210
|
+
if not isinstance(entry, str):
|
|
211
|
+
continue
|
|
212
|
+
try:
|
|
213
|
+
patterns.append(re.compile(entry))
|
|
214
|
+
except re.error as e:
|
|
215
|
+
logger.warning("Invalid suppressJdtlsPatterns regex %r: %s", entry, e)
|
|
216
|
+
return patterns
|
|
212
217
|
|
|
213
218
|
|
|
214
|
-
def
|
|
215
|
-
"""Check if a jdtls diagnostic
|
|
219
|
+
def _is_jdtls_suppressed(diag: dict[str, Any], user_patterns: list[re.Pattern[str]]) -> bool:
|
|
220
|
+
"""Check if a jdtls diagnostic matches a user-configured suppress pattern.
|
|
216
221
|
|
|
217
|
-
|
|
222
|
+
No built-in patterns — the root cause (stale jdtls caches) is fixed by
|
|
223
|
+
clearing the cache on version change. Users can still suppress specific
|
|
224
|
+
jdtls messages via suppressJdtlsPatterns in .java-functional-lsp.json.
|
|
218
225
|
"""
|
|
226
|
+
if not user_patterns:
|
|
227
|
+
return False
|
|
219
228
|
msg = diag.get("message", "")
|
|
220
|
-
|
|
229
|
+
for pat in user_patterns:
|
|
230
|
+
if pat.search(msg):
|
|
231
|
+
return True
|
|
232
|
+
return False
|
|
221
233
|
|
|
222
234
|
|
|
223
235
|
def _run_analysis(source: str, uri: str) -> list[lsp.Diagnostic]:
|
|
224
|
-
"""Run custom analyzers on source text and merge with jdtls diagnostics.
|
|
236
|
+
"""Run custom analyzers on source text and merge with jdtls diagnostics.
|
|
237
|
+
|
|
238
|
+
jdtls processing is isolated: if it fails, custom diagnostics still publish.
|
|
239
|
+
"""
|
|
225
240
|
custom_diags = _analyze_document(source, uri)
|
|
226
241
|
|
|
227
242
|
jdtls_diags: list[lsp.Diagnostic] = []
|
|
228
243
|
if server._proxy.is_available:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
raw = [d for d in raw if not
|
|
232
|
-
|
|
244
|
+
try:
|
|
245
|
+
raw = server._proxy.get_cached_diagnostics(uri)
|
|
246
|
+
raw = [d for d in raw if not _is_jdtls_suppressed(d, server._user_suppress_patterns)]
|
|
247
|
+
jdtls_diags = _jdtls_raw_to_lsp_diagnostics(raw)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.warning("jdtls diagnostic processing failed for %s: %s", uri, e)
|
|
233
250
|
|
|
234
251
|
return jdtls_diags + custom_diags
|
|
235
252
|
|
|
@@ -257,6 +274,7 @@ def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|
|
257
274
|
root = params.root_path
|
|
258
275
|
|
|
259
276
|
server._config = _load_config(root)
|
|
277
|
+
server._user_suppress_patterns = _compile_user_patterns(server._config)
|
|
260
278
|
|
|
261
279
|
return lsp.InitializeResult(
|
|
262
280
|
capabilities=lsp.ServerCapabilities(
|
|
@@ -363,7 +381,10 @@ def _analyze_and_publish(uri: str) -> None:
|
|
|
363
381
|
async def _deferred_validate(uri: str) -> None:
|
|
364
382
|
"""Debounced validation — waits before analyzing to batch rapid edits."""
|
|
365
383
|
await asyncio.sleep(_DEBOUNCE_SECONDS)
|
|
366
|
-
|
|
384
|
+
try:
|
|
385
|
+
_analyze_and_publish(uri)
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.error("Validation failed for %s: %s", uri, e)
|
|
367
388
|
|
|
368
389
|
|
|
369
390
|
def _forward_or_queue(method: str, serialized: Any) -> None:
|
|
@@ -402,7 +423,10 @@ async def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
|
|
402
423
|
task.add_done_callback(_bg_tasks.discard)
|
|
403
424
|
|
|
404
425
|
# Custom diagnostics always publish immediately — never blocked by jdtls.
|
|
405
|
-
|
|
426
|
+
try:
|
|
427
|
+
_analyze_and_publish(uri)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error("Analysis failed on open for %s: %s", uri, e)
|
|
406
430
|
|
|
407
431
|
|
|
408
432
|
@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
|
|
@@ -420,7 +444,10 @@ async def on_did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
|
|
420
444
|
async def on_did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
|
421
445
|
"""Forward to jdtls and re-analyze immediately (no debounce on save)."""
|
|
422
446
|
_forward_or_queue("textDocument/didSave", _serialize_params(params))
|
|
423
|
-
|
|
447
|
+
try:
|
|
448
|
+
_analyze_and_publish(params.text_document.uri)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.error("Analysis failed on save for %s: %s", params.text_document.uri, e)
|
|
424
451
|
|
|
425
452
|
|
|
426
453
|
@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
|
@@ -647,55 +647,107 @@ 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
|
|
696
740
|
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
699
751
|
|
|
700
752
|
|
|
701
753
|
class TestFindLombokJar:
|
|
@@ -761,6 +813,51 @@ class TestFindLombokJar:
|
|
|
761
813
|
assert any("does not exist" in r.getMessage() for r in caplog.records)
|
|
762
814
|
|
|
763
815
|
|
|
816
|
+
class TestCacheClear:
|
|
817
|
+
"""Tests for _clear_cache_on_version_change()."""
|
|
818
|
+
|
|
819
|
+
def test_clears_on_version_mismatch(self, tmp_path: Any) -> None:
|
|
820
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
821
|
+
|
|
822
|
+
cache = tmp_path / "jdtls-data"
|
|
823
|
+
cache.mkdir()
|
|
824
|
+
stale_dir = cache / "abc123"
|
|
825
|
+
stale_dir.mkdir()
|
|
826
|
+
(cache / ".version").write_text("0.7.9")
|
|
827
|
+
|
|
828
|
+
_clear_cache_on_version_change(cache)
|
|
829
|
+
|
|
830
|
+
assert not stale_dir.exists(), "Stale data-dir should be cleared"
|
|
831
|
+
assert cache.exists(), "Cache root should be recreated"
|
|
832
|
+
from java_functional_lsp import __version__
|
|
833
|
+
|
|
834
|
+
assert (cache / ".version").read_text() == __version__
|
|
835
|
+
|
|
836
|
+
def test_keeps_cache_on_same_version(self, tmp_path: Any) -> None:
|
|
837
|
+
from java_functional_lsp import __version__
|
|
838
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
839
|
+
|
|
840
|
+
cache = tmp_path / "jdtls-data"
|
|
841
|
+
cache.mkdir()
|
|
842
|
+
existing_dir = cache / "abc123"
|
|
843
|
+
existing_dir.mkdir()
|
|
844
|
+
(cache / ".version").write_text(__version__)
|
|
845
|
+
|
|
846
|
+
_clear_cache_on_version_change(cache)
|
|
847
|
+
|
|
848
|
+
assert existing_dir.exists(), "Existing data-dir should be preserved"
|
|
849
|
+
|
|
850
|
+
def test_creates_marker_on_first_run(self, tmp_path: Any) -> None:
|
|
851
|
+
from java_functional_lsp.proxy import _clear_cache_on_version_change
|
|
852
|
+
|
|
853
|
+
cache = tmp_path / "jdtls-data"
|
|
854
|
+
_clear_cache_on_version_change(cache)
|
|
855
|
+
|
|
856
|
+
from java_functional_lsp import __version__
|
|
857
|
+
|
|
858
|
+
assert (cache / ".version").read_text() == __version__
|
|
859
|
+
|
|
860
|
+
|
|
764
861
|
# Subprocess-based tests — zero mocks, real LSP transport
|
|
765
862
|
# --------------------------------------------------------------------------
|
|
766
863
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.github/ISSUE_TEMPLATE/bug-report.md
RENAMED
|
File without changes
|
{java_functional_lsp-0.7.9 → java_functional_lsp-0.8.0}/.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.0}/.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.0}/.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.7.9 → java_functional_lsp-0.8.0}/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.0}/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
|