java-functional-lsp 0.11.0__tar.gz → 0.11.2__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.11.0 → java_functional_lsp-0.11.2}/PKG-INFO +39 -10
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/README.md +38 -9
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/SKILL.md +17 -9
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/intellij/README.md +1 -1
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/README.md +1 -1
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/package-lock.json +15 -16
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/hooks/hooks.json +11 -1
- java_functional_lsp-0.11.2/hooks/post_tool_lint.py +89 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/pyproject.toml +2 -2
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/base.py +68 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/functional_checker.py +188 -25
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/mutation_checker.py +23 -27
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/fixes.py +17 -23
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_base.py +63 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_functional_checker.py +172 -0
- java_functional_lsp-0.11.2/tests/test_hooks.py +102 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_mutation_checker.py +65 -0
- java_functional_lsp-0.11.2/tests/test_version.py +26 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/uv.lock +1 -1
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.gitignore +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/LICENSE +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/__init__.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/handler_wiring.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/negotiator.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/probe.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/registry.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/static_builder.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/merkle.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/proxy.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/server.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/__init__.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/conftest.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/__init__.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_handler_wiring.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_negotiator.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_probe.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_registry.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_static_builder.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_config.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_e2e_jdtls.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_merkle.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_merkle_proxy.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_proxy.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_server.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/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.11.
|
|
3
|
+
Version: 0.11.2
|
|
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
|
|
@@ -36,7 +36,7 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
A Java Language Server that provides three things in one:
|
|
37
37
|
|
|
38
38
|
1. **Full Java language support** — completions, hover, go-to-definition, compile errors, missing imports — by proxying [Eclipse jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) under the hood
|
|
39
|
-
2. **
|
|
39
|
+
2. **17 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
|
|
40
40
|
3. **Code actions (quick fixes)** — automated refactoring via LSP `textDocument/codeAction`, with machine-readable diagnostic metadata for AI agents
|
|
41
41
|
|
|
42
42
|
Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
|
|
@@ -52,7 +52,7 @@ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the
|
|
|
52
52
|
- Type mismatches
|
|
53
53
|
- Completions, hover, go-to-definition, find references
|
|
54
54
|
|
|
55
|
-
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the
|
|
55
|
+
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the 17 custom rules still work, but you won't get compile errors or completions.
|
|
56
56
|
|
|
57
57
|
### Functional programming rules
|
|
58
58
|
|
|
@@ -66,14 +66,15 @@ Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server au
|
|
|
66
66
|
| `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` | — |
|
|
67
67
|
| `mutable-variable` | Local variable reassignment | Final variables + functional transforms | — |
|
|
68
68
|
| `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()`/`.foldLeft()` | — |
|
|
69
|
-
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) |
|
|
70
|
-
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
|
|
69
|
+
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) | ✅ |
|
|
70
|
+
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` | ✅ |
|
|
71
71
|
| `field-injection` | `@Autowired` on field | Constructor injection | — |
|
|
72
72
|
| `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
|
|
73
73
|
| `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
|
|
74
74
|
| `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
|
|
75
75
|
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
|
|
76
|
-
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
|
|
76
|
+
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` / return `Either.left` instead of throwing | — |
|
|
77
|
+
| `option-map-nullable` | `Option.map(x -> x.get(k))` followed by chained call (`Some(null)` risk) | `.flatMap(x -> Option.of(...))` | — |
|
|
77
78
|
|
|
78
79
|
## Install
|
|
79
80
|
|
|
@@ -157,7 +158,30 @@ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatic
|
|
|
157
158
|
claude plugin add https://github.com/aviadshiber/java-functional-lsp.git
|
|
158
159
|
```
|
|
159
160
|
|
|
160
|
-
This registers the LSP server, adds auto-install hooks, a PostToolUse hook that
|
|
161
|
+
This registers the LSP server, adds auto-install hooks, a PostToolUse hook that lints every `.java` file after Edit/Write and feeds the violations back to Claude as context (plus a reminder hook on Read), and the `/lint-java` command.
|
|
162
|
+
|
|
163
|
+
**Manual hook setup (without the plugin)** — add the lint hook to `~/.claude/settings.json`, pointing at a checkout of this repo:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"hooks": {
|
|
168
|
+
"PostToolUse": [
|
|
169
|
+
{
|
|
170
|
+
"matcher": "Edit|MultiEdit|Write",
|
|
171
|
+
"hooks": [
|
|
172
|
+
{
|
|
173
|
+
"type": "command",
|
|
174
|
+
"command": "python3 /path/to/java-functional-lsp/hooks/post_tool_lint.py",
|
|
175
|
+
"timeout": 10
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The hook is failure-safe: it only fires on `.java` files, lints just the edited file (well under 2s), stays silent when the file is clean, and always exits 0 so a linter problem can never break the editing session.
|
|
161
185
|
|
|
162
186
|
Or manually add to your Claude Code config:
|
|
163
187
|
|
|
@@ -341,8 +365,10 @@ The server provides LSP code actions (`textDocument/codeAction`) that automatica
|
|
|
341
365
|
| `null-check-to-monadic` | Convert to Option monadic flow | Rewrites `if (x != null) { return x.foo(); }` → `Option.of(x).map(...)`, supports chained fallbacks via `.orElse()`, adds import |
|
|
342
366
|
| `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
|
|
343
367
|
| `try-catch-to-monadic` | Convert try/catch to Try monadic flow | Rewrites `try { return expr; } catch (E e) { return default; }` → `Try.of(() -> expr).getOrElse(default)`. Supports 3 patterns: simple default (eager/lazy `.getOrElse`), logging + default (`.onFailure().getOrElse`), and exception-dependent recovery (`.recover(E.class, ...).get()`). Skips try-with-resources, finally, multi-catch, and union types. Adds import. |
|
|
368
|
+
| `imperative-option-unwrap` | Convert to Option.map().getOrElse() | Rewrites `if (opt.isDefined()) return opt.get(); else return X;` → `return opt.map(it -> ...).getOrElse(X);` (lazy `getOrElse(() -> ...)` for non-eager defaults). Bails on missing else or complex bodies. |
|
|
369
|
+
| `mutable-dto` | Replace @Data with @Value | Replaces the `@Data` annotation with `@Value` and adds `import lombok.Value`. Skips `@Setter`, `@ConfigurationProperties`, and conflicting Lombok constructor annotations. |
|
|
344
370
|
|
|
345
|
-
Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
|
|
371
|
+
Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config (`"autoImportLombok": false` for the Lombok import added by the `mutable-dto` fix).
|
|
346
372
|
|
|
347
373
|
## Agent mode (AI integration)
|
|
348
374
|
|
|
@@ -355,12 +381,14 @@ Every diagnostic includes a machine-readable `data` payload designed for AI agen
|
|
|
355
381
|
"data": {
|
|
356
382
|
"fixType": "REPLACE_WITH_VAVR_LIST",
|
|
357
383
|
"targetLibrary": "io.vavr.collection.List",
|
|
358
|
-
"rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability."
|
|
384
|
+
"rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability.",
|
|
385
|
+
"recommendedApi": ".append / .appendAll / .update / .remove (returns a new persistent collection)",
|
|
386
|
+
"suggestedSnippet": "list = list.append(\"c\"); // returns a new persistent collection"
|
|
359
387
|
}
|
|
360
388
|
}
|
|
361
389
|
```
|
|
362
390
|
|
|
363
|
-
This lets agents confidently apply fixes without guessing libraries or patterns — the `fixType` tells them *what* to do, `targetLibrary` tells them *which dependency*, and `rationale` tells them *why*.
|
|
391
|
+
This lets agents confidently apply fixes without guessing libraries or patterns — the `fixType` tells them *what* to do, `targetLibrary` tells them *which dependency*, and `rationale` tells them *why*. `recommendedApi` names the exact method on the target library (e.g. Vavr `Option` uses `forEach`, **not** `ifPresent`) and `suggestedSnippet` is a paste-able fix built from the real AST variable names.
|
|
364
392
|
|
|
365
393
|
**Agent mode configuration** in `.java-functional-lsp.json`:
|
|
366
394
|
|
|
@@ -374,6 +402,7 @@ This lets agents confidently apply fixes without guessing libraries or patterns
|
|
|
374
402
|
| Key | Default | Effect |
|
|
375
403
|
|-----|---------|--------|
|
|
376
404
|
| `autoImportVavr` | `true` | Quick fixes auto-add Vavr/Option imports |
|
|
405
|
+
| `autoImportLombok` | `true` | The `mutable-dto` quick fix auto-adds `import lombok.Value` |
|
|
377
406
|
| `strictPurity` | `false` | When `true`, `impure-method` uses WARNING severity instead of HINT |
|
|
378
407
|
|
|
379
408
|
> **Note:** The machine-readable `data` payload is always included in diagnostics when available — no configuration needed.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
A Java Language Server that provides three things in one:
|
|
9
9
|
|
|
10
10
|
1. **Full Java language support** — completions, hover, go-to-definition, compile errors, missing imports — by proxying [Eclipse jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) under the hood
|
|
11
|
-
2. **
|
|
11
|
+
2. **17 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
|
|
12
12
|
3. **Code actions (quick fixes)** — automated refactoring via LSP `textDocument/codeAction`, with machine-readable diagnostic metadata for AI agents
|
|
13
13
|
|
|
14
14
|
Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
|
|
@@ -24,7 +24,7 @@ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the
|
|
|
24
24
|
- Type mismatches
|
|
25
25
|
- Completions, hover, go-to-definition, find references
|
|
26
26
|
|
|
27
|
-
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the
|
|
27
|
+
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the 17 custom rules still work, but you won't get compile errors or completions.
|
|
28
28
|
|
|
29
29
|
### Functional programming rules
|
|
30
30
|
|
|
@@ -38,14 +38,15 @@ Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server au
|
|
|
38
38
|
| `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` | — |
|
|
39
39
|
| `mutable-variable` | Local variable reassignment | Final variables + functional transforms | — |
|
|
40
40
|
| `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()`/`.foldLeft()` | — |
|
|
41
|
-
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) |
|
|
42
|
-
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
|
|
41
|
+
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) | ✅ |
|
|
42
|
+
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` | ✅ |
|
|
43
43
|
| `field-injection` | `@Autowired` on field | Constructor injection | — |
|
|
44
44
|
| `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
|
|
45
45
|
| `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
|
|
46
46
|
| `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
|
|
47
47
|
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
|
|
48
|
-
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
|
|
48
|
+
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` / return `Either.left` instead of throwing | — |
|
|
49
|
+
| `option-map-nullable` | `Option.map(x -> x.get(k))` followed by chained call (`Some(null)` risk) | `.flatMap(x -> Option.of(...))` | — |
|
|
49
50
|
|
|
50
51
|
## Install
|
|
51
52
|
|
|
@@ -129,7 +130,30 @@ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatic
|
|
|
129
130
|
claude plugin add https://github.com/aviadshiber/java-functional-lsp.git
|
|
130
131
|
```
|
|
131
132
|
|
|
132
|
-
This registers the LSP server, adds auto-install hooks, a PostToolUse hook that
|
|
133
|
+
This registers the LSP server, adds auto-install hooks, a PostToolUse hook that lints every `.java` file after Edit/Write and feeds the violations back to Claude as context (plus a reminder hook on Read), and the `/lint-java` command.
|
|
134
|
+
|
|
135
|
+
**Manual hook setup (without the plugin)** — add the lint hook to `~/.claude/settings.json`, pointing at a checkout of this repo:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"hooks": {
|
|
140
|
+
"PostToolUse": [
|
|
141
|
+
{
|
|
142
|
+
"matcher": "Edit|MultiEdit|Write",
|
|
143
|
+
"hooks": [
|
|
144
|
+
{
|
|
145
|
+
"type": "command",
|
|
146
|
+
"command": "python3 /path/to/java-functional-lsp/hooks/post_tool_lint.py",
|
|
147
|
+
"timeout": 10
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The hook is failure-safe: it only fires on `.java` files, lints just the edited file (well under 2s), stays silent when the file is clean, and always exits 0 so a linter problem can never break the editing session.
|
|
133
157
|
|
|
134
158
|
Or manually add to your Claude Code config:
|
|
135
159
|
|
|
@@ -313,8 +337,10 @@ The server provides LSP code actions (`textDocument/codeAction`) that automatica
|
|
|
313
337
|
| `null-check-to-monadic` | Convert to Option monadic flow | Rewrites `if (x != null) { return x.foo(); }` → `Option.of(x).map(...)`, supports chained fallbacks via `.orElse()`, adds import |
|
|
314
338
|
| `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
|
|
315
339
|
| `try-catch-to-monadic` | Convert try/catch to Try monadic flow | Rewrites `try { return expr; } catch (E e) { return default; }` → `Try.of(() -> expr).getOrElse(default)`. Supports 3 patterns: simple default (eager/lazy `.getOrElse`), logging + default (`.onFailure().getOrElse`), and exception-dependent recovery (`.recover(E.class, ...).get()`). Skips try-with-resources, finally, multi-catch, and union types. Adds import. |
|
|
340
|
+
| `imperative-option-unwrap` | Convert to Option.map().getOrElse() | Rewrites `if (opt.isDefined()) return opt.get(); else return X;` → `return opt.map(it -> ...).getOrElse(X);` (lazy `getOrElse(() -> ...)` for non-eager defaults). Bails on missing else or complex bodies. |
|
|
341
|
+
| `mutable-dto` | Replace @Data with @Value | Replaces the `@Data` annotation with `@Value` and adds `import lombok.Value`. Skips `@Setter`, `@ConfigurationProperties`, and conflicting Lombok constructor annotations. |
|
|
316
342
|
|
|
317
|
-
Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
|
|
343
|
+
Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config (`"autoImportLombok": false` for the Lombok import added by the `mutable-dto` fix).
|
|
318
344
|
|
|
319
345
|
## Agent mode (AI integration)
|
|
320
346
|
|
|
@@ -327,12 +353,14 @@ Every diagnostic includes a machine-readable `data` payload designed for AI agen
|
|
|
327
353
|
"data": {
|
|
328
354
|
"fixType": "REPLACE_WITH_VAVR_LIST",
|
|
329
355
|
"targetLibrary": "io.vavr.collection.List",
|
|
330
|
-
"rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability."
|
|
356
|
+
"rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability.",
|
|
357
|
+
"recommendedApi": ".append / .appendAll / .update / .remove (returns a new persistent collection)",
|
|
358
|
+
"suggestedSnippet": "list = list.append(\"c\"); // returns a new persistent collection"
|
|
331
359
|
}
|
|
332
360
|
}
|
|
333
361
|
```
|
|
334
362
|
|
|
335
|
-
This lets agents confidently apply fixes without guessing libraries or patterns — the `fixType` tells them *what* to do, `targetLibrary` tells them *which dependency*, and `rationale` tells them *why*.
|
|
363
|
+
This lets agents confidently apply fixes without guessing libraries or patterns — the `fixType` tells them *what* to do, `targetLibrary` tells them *which dependency*, and `rationale` tells them *why*. `recommendedApi` names the exact method on the target library (e.g. Vavr `Option` uses `forEach`, **not** `ifPresent`) and `suggestedSnippet` is a paste-able fix built from the real AST variable names.
|
|
336
364
|
|
|
337
365
|
**Agent mode configuration** in `.java-functional-lsp.json`:
|
|
338
366
|
|
|
@@ -346,6 +374,7 @@ This lets agents confidently apply fixes without guessing libraries or patterns
|
|
|
346
374
|
| Key | Default | Effect |
|
|
347
375
|
|-----|---------|--------|
|
|
348
376
|
| `autoImportVavr` | `true` | Quick fixes auto-add Vavr/Option imports |
|
|
377
|
+
| `autoImportLombok` | `true` | The `mutable-dto` quick fix auto-adds `import lombok.Value` |
|
|
349
378
|
| `strictPurity` | `false` | When `true`, `impure-method` uses WARNING severity instead of HINT |
|
|
350
379
|
|
|
351
380
|
> **Note:** The machine-readable `data` payload is always included in diagnostics when available — no configuration needed.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: java-functional-lsp
|
|
3
|
-
description: Java LSP with full language support (completions, hover, go-to-def, compile errors) plus
|
|
3
|
+
description: Java LSP with full language support (completions, hover, go-to-def, compile errors) plus 17 functional programming rules with automated quick fixes. Auto-invoke when setting up Java language support or discussing Java linting configuration.
|
|
4
4
|
allowed-tools: Bash
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Java Functional LSP
|
|
9
9
|
|
|
10
|
-
A Java LSP server that wraps jdtls and adds
|
|
10
|
+
A Java LSP server that wraps jdtls and adds 17 functional programming rules with code actions (quick fixes). Gives you **full Java language support** (completions, hover, go-to-def, compile errors) **plus** custom diagnostics with machine-readable metadata for AI agents — all before compilation.
|
|
11
11
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ brew install jdtls
|
|
|
22
22
|
|
|
23
23
|
Without jdtls, the server runs in standalone mode — custom rules still work, but no completions/hover/compile errors.
|
|
24
24
|
|
|
25
|
-
## Rules (
|
|
25
|
+
## Rules (17 checks)
|
|
26
26
|
|
|
27
27
|
| Rule | Detects | Suggests | Quick Fix |
|
|
28
28
|
|------|---------|----------|-----------|
|
|
@@ -34,14 +34,15 @@ Without jdtls, the server runs in standalone mode — custom rules still work, b
|
|
|
34
34
|
| `catch-rethrow` | catch wraps + rethrows | `Try.of().toEither()` | — |
|
|
35
35
|
| `mutable-variable` | Variable reassignment | Final + functional transforms | — |
|
|
36
36
|
| `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()` | — |
|
|
37
|
-
| `mutable-dto` | `@Data` or `@Setter` | `@Value` (immutable) |
|
|
38
|
-
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
|
|
37
|
+
| `mutable-dto` | `@Data` or `@Setter` | `@Value` (immutable) | ✅ |
|
|
38
|
+
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` | ✅ |
|
|
39
39
|
| `field-injection` | `@Autowired` on field | Constructor injection | — |
|
|
40
40
|
| `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
|
|
41
41
|
| `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
|
|
42
42
|
| `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
|
|
43
43
|
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
|
|
44
|
-
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
|
|
44
|
+
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` / return `Either.left` instead of throwing | — |
|
|
45
|
+
| `option-map-nullable` | `Option.map(x -> x.get(k))` followed by chained call (`Some(null)` risk) | `.flatMap(x -> Option.of(...))` | — |
|
|
45
46
|
|
|
46
47
|
## Code Actions (Quick Fixes)
|
|
47
48
|
|
|
@@ -51,6 +52,8 @@ Rules marked ✅ provide automated `textDocument/codeAction` fixes:
|
|
|
51
52
|
- **null-check-to-monadic** → "Convert to Option monadic flow" — rewrites `if (x != null)` to `Option.of(x).map(...)`, supports chained fallbacks via `.orElse()`, adds import
|
|
52
53
|
- **null-return** → "Replace with Option.none()" — replaces `null` with `Option.none()`, adds import
|
|
53
54
|
- **try-catch-to-monadic** → "Convert try/catch to Try monadic flow" — rewrites `try { return expr; } catch (E e) { return default; }` to `Try.of(() -> expr).getOrElse(default)`. Supports 3 patterns: simple default, logging + default (`.onFailure().getOrElse`), and exception-dependent recovery (`.recover(E.class, ...).get()`). Skips try-with-resources, finally, multi-catch, union types. Adds import.
|
|
55
|
+
- **imperative-option-unwrap** → "Convert to Option.map().getOrElse()" — rewrites `if (opt.isDefined()) return opt.get(); else return X;` to `return opt.map(it -> ...).getOrElse(X);` (lazy supplier for non-eager defaults). Bails on missing else or complex bodies.
|
|
56
|
+
- **mutable-dto** → "Replace @Data with @Value" — swaps the annotation and adds `import lombok.Value` (disable with `"autoImportLombok": false`). Skips `@Setter`, `@ConfigurationProperties`, and conflicting Lombok constructor annotations.
|
|
54
57
|
|
|
55
58
|
## Agent-Ready Diagnostics
|
|
56
59
|
|
|
@@ -60,11 +63,13 @@ Every diagnostic includes a machine-readable `data` payload:
|
|
|
60
63
|
{
|
|
61
64
|
"fixType": "REPLACE_WITH_VAVR_LIST",
|
|
62
65
|
"targetLibrary": "io.vavr.collection.List",
|
|
63
|
-
"rationale": "Runtime mutation of List.of() causes UnsupportedOperationException."
|
|
66
|
+
"rationale": "Runtime mutation of List.of() causes UnsupportedOperationException.",
|
|
67
|
+
"recommendedApi": ".append / .appendAll / .update / .remove (returns a new persistent collection)",
|
|
68
|
+
"suggestedSnippet": "list = list.append(\"c\"); // returns a new persistent collection"
|
|
64
69
|
}
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
This lets AI agents apply fixes with confidence — `fixType` says what to do, `targetLibrary` says which dependency, `rationale` says why.
|
|
72
|
+
This lets AI agents apply fixes with confidence — `fixType` says what to do, `targetLibrary` says which dependency, `rationale` says why, `recommendedApi` names the exact method (e.g. Vavr `Option` uses `forEach`, not `ifPresent`), and `suggestedSnippet` is a paste-able fix built from real AST variable names.
|
|
68
73
|
|
|
69
74
|
## Configuration
|
|
70
75
|
|
|
@@ -93,7 +98,10 @@ Create `.java-functional-lsp.json` in your project root:
|
|
|
93
98
|
|
|
94
99
|
## Automatic Enforcement
|
|
95
100
|
|
|
96
|
-
The plugin includes
|
|
101
|
+
The plugin includes two PostToolUse hooks:
|
|
102
|
+
|
|
103
|
+
- **Edit/MultiEdit/Write** → `hooks/post_tool_lint.py` lints the edited `.java` file (single-file, <2s) and injects any violations into Claude's context so they get fixed immediately. Silent on clean files; internal errors are swallowed (always exits 0) so the editing session is never broken.
|
|
104
|
+
- **Read** → `hooks/java_linter_reminder.py` reminds Claude to act on LSP diagnostics shown for the file.
|
|
97
105
|
|
|
98
106
|
## On-Demand Linting
|
|
99
107
|
|
|
@@ -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
|
-
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
|
|
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 17 custom functional programming rules run, and they appear alongside IntelliJ's built-in inspections.
|
|
77
77
|
|
|
78
78
|
## Troubleshooting
|
|
79
79
|
|
|
@@ -57,4 +57,4 @@ This extension **coexists** with the Red Hat Java extension (`redhat.java`) and
|
|
|
57
57
|
|
|
58
58
|
## Rules
|
|
59
59
|
|
|
60
|
-
See the [main README](../../README.md) for the full list of
|
|
60
|
+
See the [main README](../../README.md) for the full list of 17 rules and configuration options via `.java-functional-lsp.json`.
|
|
@@ -194,20 +194,29 @@
|
|
|
194
194
|
}
|
|
195
195
|
},
|
|
196
196
|
"node_modules/@azure/msal-node": {
|
|
197
|
-
"version": "5.
|
|
198
|
-
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.
|
|
199
|
-
"integrity": "sha512-
|
|
197
|
+
"version": "5.2.2",
|
|
198
|
+
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz",
|
|
199
|
+
"integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==",
|
|
200
200
|
"dev": true,
|
|
201
201
|
"license": "MIT",
|
|
202
202
|
"dependencies": {
|
|
203
|
-
"@azure/msal-common": "16.
|
|
204
|
-
"jsonwebtoken": "^9.0.0"
|
|
205
|
-
"uuid": "^8.3.0"
|
|
203
|
+
"@azure/msal-common": "16.6.2",
|
|
204
|
+
"jsonwebtoken": "^9.0.0"
|
|
206
205
|
},
|
|
207
206
|
"engines": {
|
|
208
207
|
"node": ">=20"
|
|
209
208
|
}
|
|
210
209
|
},
|
|
210
|
+
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
|
|
211
|
+
"version": "16.6.2",
|
|
212
|
+
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz",
|
|
213
|
+
"integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==",
|
|
214
|
+
"dev": true,
|
|
215
|
+
"license": "MIT",
|
|
216
|
+
"engines": {
|
|
217
|
+
"node": ">=0.8.0"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
211
220
|
"node_modules/@babel/code-frame": {
|
|
212
221
|
"version": "7.29.0",
|
|
213
222
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
|
@@ -4351,16 +4360,6 @@
|
|
|
4351
4360
|
"license": "MIT",
|
|
4352
4361
|
"optional": true
|
|
4353
4362
|
},
|
|
4354
|
-
"node_modules/uuid": {
|
|
4355
|
-
"version": "8.3.2",
|
|
4356
|
-
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
4357
|
-
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
4358
|
-
"dev": true,
|
|
4359
|
-
"license": "MIT",
|
|
4360
|
-
"bin": {
|
|
4361
|
-
"uuid": "dist/bin/uuid"
|
|
4362
|
-
}
|
|
4363
|
-
},
|
|
4364
4363
|
"node_modules/validate-npm-package-license": {
|
|
4365
4364
|
"version": "3.0.4",
|
|
4366
4365
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
|
@@ -15,13 +15,23 @@
|
|
|
15
15
|
],
|
|
16
16
|
"PostToolUse": [
|
|
17
17
|
{
|
|
18
|
-
"matcher": "Read
|
|
18
|
+
"matcher": "Read",
|
|
19
19
|
"hooks": [
|
|
20
20
|
{
|
|
21
21
|
"type": "command",
|
|
22
22
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/java_linter_reminder.py"
|
|
23
23
|
}
|
|
24
24
|
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"matcher": "Edit|MultiEdit|Write",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_lint.py",
|
|
32
|
+
"timeout": 10
|
|
33
|
+
}
|
|
34
|
+
]
|
|
25
35
|
}
|
|
26
36
|
]
|
|
27
37
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook: lint a .java file after Edit/Write and surface violations to Claude.
|
|
3
|
+
|
|
4
|
+
Reads the Claude Code PostToolUse JSON payload on stdin, runs java-functional-lsp
|
|
5
|
+
on the edited file, and emits diagnostics as ``hookSpecificOutput.additionalContext``
|
|
6
|
+
so the agent sees them in context and can fix them immediately (issue #70).
|
|
7
|
+
|
|
8
|
+
Failure-safe by design: every path exits 0 — a linter problem must never break the
|
|
9
|
+
editing session. Prefers the fast in-process import; falls back to the CLI when the
|
|
10
|
+
package is not importable under this interpreter.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import signal
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
TIMEOUT_SECONDS = 5 # hard cap; single-file analysis is typically <200ms
|
|
22
|
+
MAX_DIAGNOSTICS = 25 # keep additionalContext bounded
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _lint_in_process(path: Path) -> list[str] | None:
|
|
26
|
+
"""Lint via direct import. Returns None if the package isn't importable here."""
|
|
27
|
+
try:
|
|
28
|
+
from java_functional_lsp.analyzers.base import is_excluded
|
|
29
|
+
from java_functional_lsp.cli import check_file, format_diagnostic, load_config
|
|
30
|
+
except ImportError:
|
|
31
|
+
return None
|
|
32
|
+
config = load_config(path)
|
|
33
|
+
if is_excluded(path.as_posix(), config.get("excludes", [])):
|
|
34
|
+
return []
|
|
35
|
+
return [format_diagnostic(path, d) for d in check_file(path, config)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _lint_via_cli(path: Path) -> list[str]:
|
|
39
|
+
"""Fallback: shell out to the installed CLI (exit 1 + stdout lines on violations)."""
|
|
40
|
+
proc = subprocess.run(
|
|
41
|
+
["java-functional-lsp", "check", str(path)],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=TIMEOUT_SECONDS,
|
|
45
|
+
check=False, # exit 1 just means violations were found
|
|
46
|
+
)
|
|
47
|
+
return [ln for ln in proc.stdout.splitlines() if ln.strip()]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
hook_input = json.load(sys.stdin)
|
|
52
|
+
file_path = (hook_input.get("tool_input") or {}).get("file_path", "")
|
|
53
|
+
if not file_path.endswith(".java"):
|
|
54
|
+
return # silent no-op
|
|
55
|
+
path = Path(file_path)
|
|
56
|
+
if not path.is_file():
|
|
57
|
+
return # tool call may have failed or the file was deleted
|
|
58
|
+
|
|
59
|
+
lines = _lint_in_process(path)
|
|
60
|
+
if lines is None:
|
|
61
|
+
lines = _lint_via_cli(path)
|
|
62
|
+
if not lines:
|
|
63
|
+
return # clean file: stay silent, no per-edit context noise
|
|
64
|
+
|
|
65
|
+
if len(lines) > MAX_DIAGNOSTICS:
|
|
66
|
+
lines = [*lines[:MAX_DIAGNOSTICS], f"... and {len(lines) - MAX_DIAGNOSTICS} more"]
|
|
67
|
+
json.dump(
|
|
68
|
+
{
|
|
69
|
+
"hookSpecificOutput": {
|
|
70
|
+
"hookEventName": "PostToolUse",
|
|
71
|
+
"additionalContext": (
|
|
72
|
+
"java-functional-lsp found violations in the file you just edited:\n"
|
|
73
|
+
+ "\n".join(lines)
|
|
74
|
+
+ "\nFix each violation now with your next Edit. Do not explain or list them."
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
sys.stdout,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
if hasattr(signal, "SIGALRM"): # POSIX hard runtime cap
|
|
84
|
+
signal.signal(signal.SIGALRM, lambda *_: sys.exit(0))
|
|
85
|
+
signal.alarm(TIMEOUT_SECONDS)
|
|
86
|
+
try:
|
|
87
|
+
main()
|
|
88
|
+
except Exception:
|
|
89
|
+
sys.exit(0) # hooks must never break the session
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling==1.
|
|
2
|
+
requires = ["hatchling==1.30.1"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "java-functional-lsp"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.2"
|
|
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" }
|
{java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/base.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
|
+
import re
|
|
6
7
|
from collections.abc import Generator
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from enum import IntEnum
|
|
@@ -179,6 +180,73 @@ def references_var(node: Node, var_name: bytes) -> bool:
|
|
|
179
180
|
return False
|
|
180
181
|
|
|
181
182
|
|
|
183
|
+
def single_return_stmt(branch: Node | None) -> Node | None:
|
|
184
|
+
"""Return the lone ``return_statement`` of a branch, or None.
|
|
185
|
+
|
|
186
|
+
Accepts a block (filters comments, requires exactly one statement) or a bare
|
|
187
|
+
statement. Shared by analyzers and fix generators that rewrite
|
|
188
|
+
``if (...) return X;`` shapes.
|
|
189
|
+
"""
|
|
190
|
+
if branch is None:
|
|
191
|
+
return None
|
|
192
|
+
if branch.type == "block":
|
|
193
|
+
stmts = [c for c in branch.named_children if c.type not in IGNORED_CHILDREN]
|
|
194
|
+
else:
|
|
195
|
+
stmts = [branch]
|
|
196
|
+
if len(stmts) != 1 or stmts[0].type != "return_statement":
|
|
197
|
+
return None
|
|
198
|
+
return stmts[0]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def return_expr_node(return_stmt: Node) -> Node | None:
|
|
202
|
+
"""Return the expression node of a ``return <expr>;`` statement, or None for a bare return."""
|
|
203
|
+
children = [c for c in return_stmt.named_children if c.type not in IGNORED_CHILDREN]
|
|
204
|
+
if not children:
|
|
205
|
+
return None
|
|
206
|
+
return children[0]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def return_expr_text(return_stmt: Node) -> str | None:
|
|
210
|
+
"""Return the expression text of a ``return <expr>;`` statement, or None for a bare return."""
|
|
211
|
+
expr = return_expr_node(return_stmt)
|
|
212
|
+
if expr is None or expr.text is None:
|
|
213
|
+
return None
|
|
214
|
+
decoded: str = expr.text.decode("utf-8")
|
|
215
|
+
return decoded
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def single_return_expr_text(branch: Node | None) -> str | None:
|
|
219
|
+
"""Text of the expression in a branch that is exactly ``return <expr>;``, else None."""
|
|
220
|
+
stmt = single_return_stmt(branch)
|
|
221
|
+
if stmt is None:
|
|
222
|
+
return None
|
|
223
|
+
return return_expr_text(stmt)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# Java identifiers may contain `$`, which regex `\b` treats as a non-word character:
|
|
227
|
+
# `\b\$opt` never matches, and `\bopt` falsely matches inside `$opt`. These lookarounds
|
|
228
|
+
# treat `$` as part of the identifier alphabet so both failure modes are excluded.
|
|
229
|
+
_IDENT_BOUNDARY_START = r"(?<![\w$])"
|
|
230
|
+
_IDENT_BOUNDARY_END = r"(?![\w$])"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def rewrite_var_get_call(text: str, var_name: str, replacement: str) -> str | None:
|
|
234
|
+
"""Rewrite ``var_name.get()`` calls in ``text`` to ``replacement``.
|
|
235
|
+
|
|
236
|
+
Tolerates whitespace around the ``.`` and parens (``opt .get ()`` is valid Java).
|
|
237
|
+
Returns None when nothing was rewritten — callers treat that as "this shape
|
|
238
|
+
can't be synthesised safely" and fall back to a placeholder or bail out.
|
|
239
|
+
"""
|
|
240
|
+
pattern = re.compile(rf"{_IDENT_BOUNDARY_START}{re.escape(var_name)}\s*\.\s*get\s*\(\s*\)")
|
|
241
|
+
rewritten = pattern.sub(replacement, text)
|
|
242
|
+
return rewritten if rewritten != text else None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def rewrite_var_references(text: str, var_name: str, replacement: str) -> str:
|
|
246
|
+
"""Rewrite standalone references to ``var_name`` in ``text`` to ``replacement``."""
|
|
247
|
+
return re.sub(rf"{_IDENT_BOUNDARY_START}{re.escape(var_name)}{_IDENT_BOUNDARY_END}", replacement, text)
|
|
248
|
+
|
|
249
|
+
|
|
182
250
|
def has_error_or_missing(node: Node) -> bool:
|
|
183
251
|
"""Return True if the subtree rooted at ``node`` contains any ERROR or MISSING nodes.
|
|
184
252
|
|