java-functional-lsp 0.11.1__tar.gz → 0.12.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.
Files changed (85) hide show
  1. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.claude-plugin/plugin.json +2 -5
  2. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/publish.yml +2 -2
  3. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/release-drafter.yml +1 -1
  4. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/stale.yml +1 -1
  5. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/test.yml +5 -5
  6. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/update-homebrew.yml +3 -3
  7. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/vscode-ext.yml +2 -2
  8. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/PKG-INFO +49 -25
  9. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/README.md +47 -23
  10. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/SKILL.md +20 -10
  11. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/commands/lint-java.md +1 -1
  12. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/intellij/README.md +1 -1
  13. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/README.md +1 -1
  14. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/package-lock.json +205 -175
  15. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/package.json +1 -1
  16. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/hooks/hooks.json +11 -1
  17. java_functional_lsp-0.12.0/hooks/post_tool_lint.py +89 -0
  18. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/pyproject.toml +2 -2
  19. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/__init__.py +1 -1
  20. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/base.py +97 -0
  21. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/functional_checker.py +188 -25
  22. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/mutation_checker.py +69 -33
  23. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/fixes.py +17 -23
  24. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_base.py +63 -0
  25. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_config.py +27 -1
  26. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_functional_checker.py +188 -0
  27. java_functional_lsp-0.12.0/tests/test_hooks.py +102 -0
  28. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_mutation_checker.py +126 -0
  29. java_functional_lsp-0.12.0/tests/test_plugin_manifest.py +33 -0
  30. java_functional_lsp-0.12.0/tests/test_version.py +26 -0
  31. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/uv.lock +2 -2
  32. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.githooks/pre-commit +0 -0
  33. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.githooks/pre-push +0 -0
  34. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/CODEOWNERS +0 -0
  35. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  36. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  37. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  38. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/SECURITY.md +0 -0
  39. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/dependabot.yml +0 -0
  40. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/release-drafter.yml +0 -0
  41. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.gitignore +0 -0
  42. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/CONTRIBUTING.md +0 -0
  43. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/LICENSE +0 -0
  44. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/intellij/lsp4ij-template.json +0 -0
  45. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/.vscodeignore +0 -0
  46. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/src/extension.ts +0 -0
  47. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/tsconfig.json +0 -0
  48. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/hooks/java_linter_reminder.py +0 -0
  49. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/scripts/ensure-lsp.sh +0 -0
  50. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/scripts/generate-formula.py +0 -0
  51. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/__main__.py +0 -0
  52. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  53. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  54. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  55. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  56. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/__init__.py +0 -0
  57. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/handler_wiring.py +0 -0
  58. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/negotiator.py +0 -0
  59. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/probe.py +0 -0
  60. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/registry.py +0 -0
  61. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/static_builder.py +0 -0
  62. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/cli.py +0 -0
  63. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/merkle.py +0 -0
  64. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/proxy.py +0 -0
  65. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/server.py +0 -0
  66. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/__init__.py +0 -0
  67. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/conftest.py +0 -0
  68. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/__init__.py +0 -0
  69. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_handler_wiring.py +0 -0
  70. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_negotiator.py +0 -0
  71. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_probe.py +0 -0
  72. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_registry.py +0 -0
  73. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_static_builder.py +0 -0
  74. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_cli.py +0 -0
  75. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_e2e.py +0 -0
  76. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_e2e_jdtls.py +0 -0
  77. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_exception_checker.py +0 -0
  78. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_fixes.py +0 -0
  79. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_merkle.py +0 -0
  80. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_merkle_proxy.py +0 -0
  81. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_null_checker.py +0 -0
  82. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_proxy.py +0 -0
  83. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_server.py +0 -0
  84. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_spring_checker.py +0 -0
  85. {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_suppress.py +0 -0
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "java-functional-lsp",
3
3
  "description": "Java LSP with functional programming rules enforcement — null safety, immutability, no exceptions, Spring best practices. Wraps jdtls for full Java language support.",
4
- "version": "0.4.2",
4
+ "version": "0.4.3",
5
5
  "lspServers": {
6
6
  "java-functional": {
7
7
  "command": "java-functional-lsp",
8
8
  "extensionToLanguage": {
9
9
  ".java": "java"
10
- },
11
- "startupTimeout": 120000,
12
- "restartOnCrash": true,
13
- "maxRestarts": 5
10
+ }
14
11
  }
15
12
  }
16
13
  }
@@ -13,9 +13,9 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  environment: pypi
15
15
  steps:
16
- - uses: actions/checkout@v6
16
+ - uses: actions/checkout@v7
17
17
  - name: Set up Python
18
- uses: actions/setup-python@v6
18
+ uses: actions/setup-python@v7
19
19
  with:
20
20
  python-version: "3.12"
21
21
  - name: Install build dependencies
@@ -12,6 +12,6 @@ jobs:
12
12
  update-release-draft:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: release-drafter/release-drafter@v7
15
+ - uses: release-drafter/release-drafter@v7.6.0
16
16
  env:
17
17
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -12,7 +12,7 @@ jobs:
12
12
  stale:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/stale@v10
15
+ - uses: actions/stale@v11
16
16
  with:
17
17
  stale-issue-message: >
18
18
  This issue has been automatically marked as stale because it has not
@@ -15,9 +15,9 @@ jobs:
15
15
  os: [ubuntu-latest, macos-latest]
16
16
  python-version: ["3.10", "3.11", "3.12", "3.13"]
17
17
  steps:
18
- - uses: actions/checkout@v6
18
+ - uses: actions/checkout@v7
19
19
  - name: Set up Python ${{ matrix.python-version }}
20
- uses: actions/setup-python@v6
20
+ uses: actions/setup-python@v7
21
21
  with:
22
22
  python-version: ${{ matrix.python-version }}
23
23
  - name: Install uv
@@ -48,13 +48,13 @@ jobs:
48
48
  # AND jdtls request forwarding end-to-end. The unit matrix above
49
49
  # provides fast Python-version-specific feedback without jdtls.
50
50
  steps:
51
- - uses: actions/checkout@v6
51
+ - uses: actions/checkout@v7
52
52
  - name: Set up Python 3.12
53
- uses: actions/setup-python@v6
53
+ uses: actions/setup-python@v7
54
54
  with:
55
55
  python-version: "3.12"
56
56
  - name: Install Java 21
57
- uses: actions/setup-java@v5
57
+ uses: actions/setup-java@v6
58
58
  with:
59
59
  distribution: temurin
60
60
  java-version: "21"
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
13
13
  steps:
14
- - uses: actions/checkout@v6
14
+ - uses: actions/checkout@v7
15
15
  - name: Get release version
16
16
  id: version
17
17
  env:
@@ -20,7 +20,7 @@ jobs:
20
20
  VERSION=$(gh api "repos/${{ github.repository }}/releases/latest" --jq .tag_name)
21
21
  echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT"
22
22
  - name: Set up Python
23
- uses: actions/setup-python@v6
23
+ uses: actions/setup-python@v7
24
24
  with:
25
25
  python-version: "3.12"
26
26
  - name: Wait for PyPI availability
@@ -43,7 +43,7 @@ jobs:
43
43
  echo "Generated formula:"
44
44
  cat java-functional-lsp.rb
45
45
  - name: Checkout tap repo
46
- uses: actions/checkout@v6
46
+ uses: actions/checkout@v7
47
47
  with:
48
48
  repository: aviadshiber/homebrew-tap
49
49
  token: ${{ secrets.TAP_GITHUB_TOKEN }}
@@ -18,10 +18,10 @@ jobs:
18
18
  run:
19
19
  working-directory: editors/vscode
20
20
  steps:
21
- - uses: actions/checkout@v6
21
+ - uses: actions/checkout@v7
22
22
 
23
23
  - name: Set up Node.js
24
- uses: actions/setup-node@v6
24
+ uses: actions/setup-node@v7
25
25
  with:
26
26
  node-version: "20"
27
27
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: java-functional-lsp
3
- Version: 0.11.1
3
+ Version: 0.12.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
@@ -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); `record` instead at `sourceLevel` 16+ | ✅ |
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
 
@@ -142,10 +143,7 @@ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatic
142
143
  "lspServers": {
143
144
  "java-functional": {
144
145
  "command": "java-functional-lsp",
145
- "extensionToLanguage": { ".java": "java" },
146
- "startupTimeout": 120000,
147
- "restartOnCrash": true,
148
- "maxRestarts": 5
146
+ "extensionToLanguage": { ".java": "java" }
149
147
  }
150
148
  }
151
149
  }
@@ -157,7 +155,30 @@ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatic
157
155
  claude plugin add https://github.com/aviadshiber/java-functional-lsp.git
158
156
  ```
159
157
 
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.
158
+ 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.
159
+
160
+ **Manual hook setup (without the plugin)** — add the lint hook to `~/.claude/settings.json`, pointing at a checkout of this repo:
161
+
162
+ ```json
163
+ {
164
+ "hooks": {
165
+ "PostToolUse": [
166
+ {
167
+ "matcher": "Edit|MultiEdit|Write",
168
+ "hooks": [
169
+ {
170
+ "type": "command",
171
+ "command": "python3 /path/to/java-functional-lsp/hooks/post_tool_lint.py",
172
+ "timeout": 10
173
+ }
174
+ ]
175
+ }
176
+ ]
177
+ }
178
+ }
179
+ ```
180
+
181
+ 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
182
 
162
183
  Or manually add to your Claude Code config:
163
184
 
@@ -166,27 +187,19 @@ Or manually add to your Claude Code config:
166
187
  "lspServers": {
167
188
  "java-functional": {
168
189
  "command": "java-functional-lsp",
169
- "extensionToLanguage": { ".java": "java" },
170
- "startupTimeout": 120000,
171
- "restartOnCrash": true,
172
- "maxRestarts": 5
190
+ "extensionToLanguage": { ".java": "java" }
173
191
  }
174
192
  }
175
193
  }
176
194
  ```
177
195
 
178
- (`startupTimeout: 120000` accommodates jdtls cold-start; `restartOnCrash` keeps the server alive across session.)
179
-
180
196
  **Alternative: project-level `.lsp.json`** — instead of installing the plugin or editing global config, add a `.lsp.json` file at your project root:
181
197
 
182
198
  ```json
183
199
  {
184
200
  "java-functional": {
185
201
  "command": "java-functional-lsp",
186
- "extensionToLanguage": { ".java": "java" },
187
- "startupTimeout": 120000,
188
- "restartOnCrash": true,
189
- "maxRestarts": 5
202
+ "extensionToLanguage": { ".java": "java" }
190
203
  }
191
204
  }
192
205
  ```
@@ -234,6 +247,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
234
247
  ```json
235
248
  {
236
249
  "excludes": ["**/generated/**", "**/vendor/**"],
250
+ "sourceLevel": 17,
237
251
  "rules": {
238
252
  "null-literal-arg": "warning",
239
253
  "throw-statement": "info",
@@ -246,12 +260,16 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
246
260
  **Options:**
247
261
  - `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
248
262
  - `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
263
+ - `sourceLevel` — the project's Java language/source level, as an int (e.g. `17`) or version string (`"17"`, legacy `"1.8"`). Also accepted as `javaVersion`. Defaults to `8` if unset, so existing configs are unaffected. Currently only changes the `mutable-dto` recommendation (see below); higher source levels unlock more rewrite targets over time (records, sealed types, pattern matching, text blocks).
249
264
  - `suppressJdtlsPatterns` — list of regex patterns to suppress jdtls diagnostics (see below)
250
265
 
251
266
  **Spring-aware behavior:**
252
267
  - `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
253
268
  - `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
254
269
 
270
+ **Source-level-aware behavior:**
271
+ - `mutable-dto` recommends a `record` instead of `@Value` once `sourceLevel` is 16 or higher (records became final/non-preview in JDK 16) — a plain immutable DTO is simpler as a built-in `record` with no Lombok dependency. It still calls out `@Value` as the fallback when the class needs `@With`/`@Builder`/`@Jacksonized`, is used as a facade, or relies on AOP proxying (records are `final` and can't be proxied). Below `sourceLevel` 16 (including the default), the message and quick fix are unchanged — `@Value` only.
272
+
255
273
  **Inline suppression** with `@SuppressWarnings`:
256
274
 
257
275
  ```java
@@ -341,8 +359,10 @@ The server provides LSP code actions (`textDocument/codeAction`) that automatica
341
359
  | `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
360
  | `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
343
361
  | `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. |
362
+ | `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. |
363
+ | `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. This quick fix is currently `@Value`-only regardless of `sourceLevel` — the diagnostic message/snippet recommend a `record` at `sourceLevel` 16+, but applying that rewrite safely (enumerating fields into record components) isn't automated yet; use the suggested snippet as a manual starting point. |
344
364
 
345
- Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
365
+ 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
366
 
347
367
  ## Agent mode (AI integration)
348
368
 
@@ -355,12 +375,14 @@ Every diagnostic includes a machine-readable `data` payload designed for AI agen
355
375
  "data": {
356
376
  "fixType": "REPLACE_WITH_VAVR_LIST",
357
377
  "targetLibrary": "io.vavr.collection.List",
358
- "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability."
378
+ "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability.",
379
+ "recommendedApi": ".append / .appendAll / .update / .remove (returns a new persistent collection)",
380
+ "suggestedSnippet": "list = list.append(\"c\"); // returns a new persistent collection"
359
381
  }
360
382
  }
361
383
  ```
362
384
 
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*.
385
+ 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
386
 
365
387
  **Agent mode configuration** in `.java-functional-lsp.json`:
366
388
 
@@ -374,7 +396,9 @@ This lets agents confidently apply fixes without guessing libraries or patterns
374
396
  | Key | Default | Effect |
375
397
  |-----|---------|--------|
376
398
  | `autoImportVavr` | `true` | Quick fixes auto-add Vavr/Option imports |
399
+ | `autoImportLombok` | `true` | The `mutable-dto` quick fix auto-adds `import lombok.Value` |
377
400
  | `strictPurity` | `false` | When `true`, `impure-method` uses WARNING severity instead of HINT |
401
+ | `sourceLevel` (alias `javaVersion`) | `8` | Java source level; `mutable-dto` recommends `record` over `@Value` at 16+ |
378
402
 
379
403
  > **Note:** The machine-readable `data` payload is always included in diagnostics when available — no configuration needed.
380
404
 
@@ -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); `record` instead at `sourceLevel` 16+ | ✅ |
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
 
@@ -114,10 +115,7 @@ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatic
114
115
  "lspServers": {
115
116
  "java-functional": {
116
117
  "command": "java-functional-lsp",
117
- "extensionToLanguage": { ".java": "java" },
118
- "startupTimeout": 120000,
119
- "restartOnCrash": true,
120
- "maxRestarts": 5
118
+ "extensionToLanguage": { ".java": "java" }
121
119
  }
122
120
  }
123
121
  }
@@ -129,7 +127,30 @@ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatic
129
127
  claude plugin add https://github.com/aviadshiber/java-functional-lsp.git
130
128
  ```
131
129
 
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.
130
+ 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.
131
+
132
+ **Manual hook setup (without the plugin)** — add the lint hook to `~/.claude/settings.json`, pointing at a checkout of this repo:
133
+
134
+ ```json
135
+ {
136
+ "hooks": {
137
+ "PostToolUse": [
138
+ {
139
+ "matcher": "Edit|MultiEdit|Write",
140
+ "hooks": [
141
+ {
142
+ "type": "command",
143
+ "command": "python3 /path/to/java-functional-lsp/hooks/post_tool_lint.py",
144
+ "timeout": 10
145
+ }
146
+ ]
147
+ }
148
+ ]
149
+ }
150
+ }
151
+ ```
152
+
153
+ 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
154
 
134
155
  Or manually add to your Claude Code config:
135
156
 
@@ -138,27 +159,19 @@ Or manually add to your Claude Code config:
138
159
  "lspServers": {
139
160
  "java-functional": {
140
161
  "command": "java-functional-lsp",
141
- "extensionToLanguage": { ".java": "java" },
142
- "startupTimeout": 120000,
143
- "restartOnCrash": true,
144
- "maxRestarts": 5
162
+ "extensionToLanguage": { ".java": "java" }
145
163
  }
146
164
  }
147
165
  }
148
166
  ```
149
167
 
150
- (`startupTimeout: 120000` accommodates jdtls cold-start; `restartOnCrash` keeps the server alive across session.)
151
-
152
168
  **Alternative: project-level `.lsp.json`** — instead of installing the plugin or editing global config, add a `.lsp.json` file at your project root:
153
169
 
154
170
  ```json
155
171
  {
156
172
  "java-functional": {
157
173
  "command": "java-functional-lsp",
158
- "extensionToLanguage": { ".java": "java" },
159
- "startupTimeout": 120000,
160
- "restartOnCrash": true,
161
- "maxRestarts": 5
174
+ "extensionToLanguage": { ".java": "java" }
162
175
  }
163
176
  }
164
177
  ```
@@ -206,6 +219,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
206
219
  ```json
207
220
  {
208
221
  "excludes": ["**/generated/**", "**/vendor/**"],
222
+ "sourceLevel": 17,
209
223
  "rules": {
210
224
  "null-literal-arg": "warning",
211
225
  "throw-statement": "info",
@@ -218,12 +232,16 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
218
232
  **Options:**
219
233
  - `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
220
234
  - `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
235
+ - `sourceLevel` — the project's Java language/source level, as an int (e.g. `17`) or version string (`"17"`, legacy `"1.8"`). Also accepted as `javaVersion`. Defaults to `8` if unset, so existing configs are unaffected. Currently only changes the `mutable-dto` recommendation (see below); higher source levels unlock more rewrite targets over time (records, sealed types, pattern matching, text blocks).
221
236
  - `suppressJdtlsPatterns` — list of regex patterns to suppress jdtls diagnostics (see below)
222
237
 
223
238
  **Spring-aware behavior:**
224
239
  - `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
225
240
  - `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
226
241
 
242
+ **Source-level-aware behavior:**
243
+ - `mutable-dto` recommends a `record` instead of `@Value` once `sourceLevel` is 16 or higher (records became final/non-preview in JDK 16) — a plain immutable DTO is simpler as a built-in `record` with no Lombok dependency. It still calls out `@Value` as the fallback when the class needs `@With`/`@Builder`/`@Jacksonized`, is used as a facade, or relies on AOP proxying (records are `final` and can't be proxied). Below `sourceLevel` 16 (including the default), the message and quick fix are unchanged — `@Value` only.
244
+
227
245
  **Inline suppression** with `@SuppressWarnings`:
228
246
 
229
247
  ```java
@@ -313,8 +331,10 @@ The server provides LSP code actions (`textDocument/codeAction`) that automatica
313
331
  | `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
332
  | `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
315
333
  | `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. |
334
+ | `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. |
335
+ | `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. This quick fix is currently `@Value`-only regardless of `sourceLevel` — the diagnostic message/snippet recommend a `record` at `sourceLevel` 16+, but applying that rewrite safely (enumerating fields into record components) isn't automated yet; use the suggested snippet as a manual starting point. |
316
336
 
317
- Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
337
+ 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
338
 
319
339
  ## Agent mode (AI integration)
320
340
 
@@ -327,12 +347,14 @@ Every diagnostic includes a machine-readable `data` payload designed for AI agen
327
347
  "data": {
328
348
  "fixType": "REPLACE_WITH_VAVR_LIST",
329
349
  "targetLibrary": "io.vavr.collection.List",
330
- "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability."
350
+ "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability.",
351
+ "recommendedApi": ".append / .appendAll / .update / .remove (returns a new persistent collection)",
352
+ "suggestedSnippet": "list = list.append(\"c\"); // returns a new persistent collection"
331
353
  }
332
354
  }
333
355
  ```
334
356
 
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*.
357
+ 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
358
 
337
359
  **Agent mode configuration** in `.java-functional-lsp.json`:
338
360
 
@@ -346,7 +368,9 @@ This lets agents confidently apply fixes without guessing libraries or patterns
346
368
  | Key | Default | Effect |
347
369
  |-----|---------|--------|
348
370
  | `autoImportVavr` | `true` | Quick fixes auto-add Vavr/Option imports |
371
+ | `autoImportLombok` | `true` | The `mutable-dto` quick fix auto-adds `import lombok.Value` |
349
372
  | `strictPurity` | `false` | When `true`, `impure-method` uses WARNING severity instead of HINT |
373
+ | `sourceLevel` (alias `javaVersion`) | `8` | Java source level; `mutable-dto` recommends `record` over `@Value` at 16+ |
350
374
 
351
375
  > **Note:** The machine-readable `data` payload is always included in diagnostics when available — no configuration needed.
352
376
 
@@ -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` | `record` (source level 16+) or `@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. At source level 16+ the *diagnostic* recommends a `record` for a plain immutable DTO (with `@Value` as the fallback for `@With`/`@Builder`/`@Jacksonized`/facade/AOP cases); the automated fix still applies `@Value`, so convert to a `record` manually where appropriate.
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
 
@@ -72,6 +77,7 @@ Create `.java-functional-lsp.json` in your project root:
72
77
 
73
78
  ```json
74
79
  {
80
+ "sourceLevel": 17,
75
81
  "excludes": ["**/generated/**", "**/vendor/**"],
76
82
  "rules": {
77
83
  "imperative-loop": "hint",
@@ -83,6 +89,7 @@ Create `.java-functional-lsp.json` in your project root:
83
89
  }
84
90
  ```
85
91
 
92
+ - `sourceLevel` (alias `javaVersion`) — the project's Java source level, as an int (`17`) or string (`"17"`, legacy `"1.8"` → 8). Defaults to `8` if unset (existing configs unaffected). At `16+`, `mutable-dto` recommends a `record` over `@Value` for plain DTOs
86
93
  - `excludes` — glob patterns to skip files/directories entirely
87
94
  - `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
88
95
  - `autoImportVavr` — quick fixes auto-add Vavr imports (default: `true`)
@@ -93,7 +100,10 @@ Create `.java-functional-lsp.json` in your project root:
93
100
 
94
101
  ## Automatic Enforcement
95
102
 
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.
103
+ The plugin includes two PostToolUse hooks:
104
+
105
+ - **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.
106
+ - **Read** → `hooks/java_linter_reminder.py` reminds Claude to act on LSP diagnostics shown for the file.
97
107
 
98
108
  ## On-Demand Linting
99
109
 
@@ -116,7 +126,7 @@ Declare `lspServers` in `~/.claude/settings.json` or in `plugin.json` — Claude
116
126
 
117
127
  For containers or CI, add a `.lsp.json` at the project root instead of installing the plugin:
118
128
  ```json
119
- { "java-functional": { "command": "java-functional-lsp", "extensionToLanguage": { ".java": "java" }, "startupTimeout": 120000, "restartOnCrash": true, "maxRestarts": 5 } }
129
+ { "java-functional": { "command": "java-functional-lsp", "extensionToLanguage": { ".java": "java" } } }
120
130
  ```
121
131
 
122
132
  To nudge Claude to act on diagnostics, add to your project's `CLAUDE.md`:
@@ -35,6 +35,6 @@ $ARGUMENTS - Java files, directories, or glob patterns to lint. If empty, lint a
35
35
  - `throw-statement` → convert to `Either.left()` or `Try.of()`
36
36
  - `mutable-variable` → make final, use functional transforms
37
37
  - `imperative-loop` → replace with `.map()`, `.filter()`, `.flatMap()`
38
- - `mutable-dto` → change `@Data` to `@Value`
38
+ - `mutable-dto` → convert to a `record` (source level 16+) or change `@Data` to `@Value` (follow the diagnostic's recommendation, which is source-level aware)
39
39
  - `field-injection` → convert to constructor injection
40
40
  - `component-annotation` → move to `@Configuration` class with `@Bean`
@@ -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`.