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.
Files changed (84) hide show
  1. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/PKG-INFO +39 -10
  2. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/README.md +38 -9
  3. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/SKILL.md +17 -9
  4. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/intellij/README.md +1 -1
  5. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/README.md +1 -1
  6. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/package-lock.json +15 -16
  7. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/hooks/hooks.json +11 -1
  8. java_functional_lsp-0.11.2/hooks/post_tool_lint.py +89 -0
  9. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/pyproject.toml +2 -2
  10. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/__init__.py +1 -1
  11. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/base.py +68 -0
  12. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/functional_checker.py +188 -25
  13. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/mutation_checker.py +23 -27
  14. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/fixes.py +17 -23
  15. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_base.py +63 -0
  16. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_functional_checker.py +172 -0
  17. java_functional_lsp-0.11.2/tests/test_hooks.py +102 -0
  18. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_mutation_checker.py +65 -0
  19. java_functional_lsp-0.11.2/tests/test_version.py +26 -0
  20. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/uv.lock +1 -1
  21. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.claude-plugin/plugin.json +0 -0
  22. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.githooks/pre-commit +0 -0
  23. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.githooks/pre-push +0 -0
  24. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/CODEOWNERS +0 -0
  25. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  26. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  27. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/SECURITY.md +0 -0
  29. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/dependabot.yml +0 -0
  30. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/release-drafter.yml +0 -0
  31. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/publish.yml +0 -0
  32. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/release-drafter.yml +0 -0
  33. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/stale.yml +0 -0
  34. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/test.yml +0 -0
  35. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/update-homebrew.yml +0 -0
  36. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.github/workflows/vscode-ext.yml +0 -0
  37. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/.gitignore +0 -0
  38. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/CONTRIBUTING.md +0 -0
  39. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/LICENSE +0 -0
  40. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/commands/lint-java.md +0 -0
  41. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/intellij/lsp4ij-template.json +0 -0
  42. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/.vscodeignore +0 -0
  43. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/package.json +0 -0
  44. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/src/extension.ts +0 -0
  45. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/editors/vscode/tsconfig.json +0 -0
  46. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/hooks/java_linter_reminder.py +0 -0
  47. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/scripts/ensure-lsp.sh +0 -0
  48. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/scripts/generate-formula.py +0 -0
  49. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/__main__.py +0 -0
  50. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  51. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  52. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  53. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  54. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/__init__.py +0 -0
  55. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/handler_wiring.py +0 -0
  56. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/negotiator.py +0 -0
  57. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/probe.py +0 -0
  58. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/registry.py +0 -0
  59. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/capabilities/static_builder.py +0 -0
  60. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/cli.py +0 -0
  61. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/merkle.py +0 -0
  62. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/proxy.py +0 -0
  63. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/src/java_functional_lsp/server.py +0 -0
  64. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/__init__.py +0 -0
  65. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/conftest.py +0 -0
  66. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/__init__.py +0 -0
  67. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_handler_wiring.py +0 -0
  68. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_negotiator.py +0 -0
  69. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_probe.py +0 -0
  70. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_registry.py +0 -0
  71. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_capabilities/test_static_builder.py +0 -0
  72. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_cli.py +0 -0
  73. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_config.py +0 -0
  74. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_e2e.py +0 -0
  75. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_e2e_jdtls.py +0 -0
  76. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_exception_checker.py +0 -0
  77. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_fixes.py +0 -0
  78. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_merkle.py +0 -0
  79. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_merkle_proxy.py +0 -0
  80. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_null_checker.py +0 -0
  81. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_proxy.py +0 -0
  82. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_server.py +0 -0
  83. {java_functional_lsp-0.11.0 → java_functional_lsp-0.11.2}/tests/test_spring_checker.py +0 -0
  84. {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.0
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. **16 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
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 16 custom rules still work, but you won't get compile errors or completions.
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 reminds Claude to fix violations on every `.java` file edit, and the `/lint-java` command.
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. **16 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
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 16 custom rules still work, but you won't get compile errors or completions.
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 reminds Claude to fix violations on every `.java` file edit, and the `/lint-java` command.
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 16 functional programming rules with automated quick fixes. Auto-invoke when setting up Java language support or discussing Java linting configuration.
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 16 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.
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 (16 checks)
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 a PostToolUse hook that fires on every Read/Edit/Write of `.java` files. If diagnostics appear, Claude is prompted to fix them immediately without explanation.
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 16 custom functional programming rules run, and they appear alongside IntelliJ's built-in inspections.
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 12 rules and configuration options via `.java-functional-lsp.json`.
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.1.2",
198
- "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.1.2.tgz",
199
- "integrity": "sha512-DoeSJ9U5KPAIZoHsPywvfEj2MhBniQe0+FSpjLUTdWoIkI999GB5USkW6nNEHnIaLVxROHXvprWA1KzdS1VQ4A==",
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.4.1",
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|Edit|Write",
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.29.0"]
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.0"
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" }
@@ -1,3 +1,3 @@
1
1
  """java-functional-lsp: A Java LSP server enforcing functional programming best practices."""
2
2
 
3
- __version__ = "0.10.0"
3
+ __version__ = "0.11.2"
@@ -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