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.
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.claude-plugin/plugin.json +2 -5
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/publish.yml +2 -2
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/release-drafter.yml +1 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/stale.yml +1 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/test.yml +5 -5
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/update-homebrew.yml +3 -3
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/vscode-ext.yml +2 -2
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/PKG-INFO +49 -25
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/README.md +47 -23
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/SKILL.md +20 -10
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/commands/lint-java.md +1 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/intellij/README.md +1 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/README.md +1 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/package-lock.json +205 -175
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/package.json +1 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/hooks/hooks.json +11 -1
- java_functional_lsp-0.12.0/hooks/post_tool_lint.py +89 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/pyproject.toml +2 -2
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/base.py +97 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/functional_checker.py +188 -25
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/mutation_checker.py +69 -33
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/fixes.py +17 -23
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_base.py +63 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_config.py +27 -1
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_functional_checker.py +188 -0
- java_functional_lsp-0.12.0/tests/test_hooks.py +102 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_mutation_checker.py +126 -0
- java_functional_lsp-0.12.0/tests/test_plugin_manifest.py +33 -0
- java_functional_lsp-0.12.0/tests/test_version.py +26 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/uv.lock +2 -2
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.gitignore +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/LICENSE +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/__init__.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/handler_wiring.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/negotiator.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/probe.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/registry.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/capabilities/static_builder.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/merkle.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/proxy.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/src/java_functional_lsp/server.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/__init__.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/conftest.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/__init__.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_handler_wiring.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_negotiator.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_probe.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_registry.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_capabilities/test_static_builder.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_e2e_jdtls.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_merkle.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_merkle_proxy.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_proxy.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_server.py +0 -0
- {java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/tests/test_spring_checker.py +0 -0
- {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.
|
|
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@
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
17
|
- name: Set up Python
|
|
18
|
-
uses: actions/setup-python@
|
|
18
|
+
uses: actions/setup-python@v7
|
|
19
19
|
with:
|
|
20
20
|
python-version: "3.12"
|
|
21
21
|
- name: Install build dependencies
|
|
@@ -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@
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
19
|
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
-
uses: actions/setup-python@
|
|
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@
|
|
51
|
+
- uses: actions/checkout@v7
|
|
52
52
|
- name: Set up Python 3.12
|
|
53
|
-
uses: actions/setup-python@
|
|
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@
|
|
57
|
+
uses: actions/setup-java@v6
|
|
58
58
|
with:
|
|
59
59
|
distribution: temurin
|
|
60
60
|
java-version: "21"
|
{java_functional_lsp-0.11.1 → java_functional_lsp-0.12.0}/.github/workflows/update-homebrew.yml
RENAMED
|
@@ -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@
|
|
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@
|
|
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@
|
|
46
|
+
uses: actions/checkout@v7
|
|
47
47
|
with:
|
|
48
48
|
repository: aviadshiber/homebrew-tap
|
|
49
49
|
token: ${{ secrets.TAP_GITHUB_TOKEN }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: java-functional-lsp
|
|
3
|
-
Version: 0.
|
|
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. **
|
|
39
|
+
2. **17 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
|
|
40
40
|
3. **Code actions (quick fixes)** — automated refactoring via LSP `textDocument/codeAction`, with machine-readable diagnostic metadata for AI agents
|
|
41
41
|
|
|
42
42
|
Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
|
|
@@ -52,7 +52,7 @@ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the
|
|
|
52
52
|
- Type mismatches
|
|
53
53
|
- Completions, hover, go-to-definition, find references
|
|
54
54
|
|
|
55
|
-
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the
|
|
55
|
+
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the 17 custom rules still work, but you won't get compile errors or completions.
|
|
56
56
|
|
|
57
57
|
### Functional programming rules
|
|
58
58
|
|
|
@@ -66,14 +66,15 @@ Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server au
|
|
|
66
66
|
| `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` | — |
|
|
67
67
|
| `mutable-variable` | Local variable reassignment | Final variables + functional transforms | — |
|
|
68
68
|
| `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()`/`.foldLeft()` | — |
|
|
69
|
-
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) |
|
|
70
|
-
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
|
|
69
|
+
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable); `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
|
|
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. **
|
|
11
|
+
2. **17 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
|
|
12
12
|
3. **Code actions (quick fixes)** — automated refactoring via LSP `textDocument/codeAction`, with machine-readable diagnostic metadata for AI agents
|
|
13
13
|
|
|
14
14
|
Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
|
|
@@ -24,7 +24,7 @@ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the
|
|
|
24
24
|
- Type mismatches
|
|
25
25
|
- Completions, hover, go-to-definition, find references
|
|
26
26
|
|
|
27
|
-
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the
|
|
27
|
+
Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server auto-detects a Java 21+ installation even when the IDE's project SDK is older (e.g., Java 8) by probing `JDTLS_JAVA_HOME`, `JAVA_HOME`, `/usr/libexec/java_home -v 21+` (macOS), and `java` on PATH. Without jdtls, the server runs in standalone mode — the 17 custom rules still work, but you won't get compile errors or completions.
|
|
28
28
|
|
|
29
29
|
### Functional programming rules
|
|
30
30
|
|
|
@@ -38,14 +38,15 @@ Install jdtls separately: `brew install jdtls` (requires JDK 21+). The server au
|
|
|
38
38
|
| `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` | — |
|
|
39
39
|
| `mutable-variable` | Local variable reassignment | Final variables + functional transforms | — |
|
|
40
40
|
| `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()`/`.foldLeft()` | — |
|
|
41
|
-
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) |
|
|
42
|
-
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
|
|
41
|
+
| `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable); `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
|
|
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
|
|
3
|
+
description: Java LSP with full language support (completions, hover, go-to-def, compile errors) plus 17 functional programming rules with automated quick fixes. Auto-invoke when setting up Java language support or discussing Java linting configuration.
|
|
4
4
|
allowed-tools: Bash
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Java Functional LSP
|
|
9
9
|
|
|
10
|
-
A Java LSP server that wraps jdtls and adds
|
|
10
|
+
A Java LSP server that wraps jdtls and adds 17 functional programming rules with code actions (quick fixes). Gives you **full Java language support** (completions, hover, go-to-def, compile errors) **plus** custom diagnostics with machine-readable metadata for AI agents — all before compilation.
|
|
11
11
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ brew install jdtls
|
|
|
22
22
|
|
|
23
23
|
Without jdtls, the server runs in standalone mode — custom rules still work, but no completions/hover/compile errors.
|
|
24
24
|
|
|
25
|
-
## Rules (
|
|
25
|
+
## Rules (17 checks)
|
|
26
26
|
|
|
27
27
|
| Rule | Detects | Suggests | Quick Fix |
|
|
28
28
|
|------|---------|----------|-----------|
|
|
@@ -34,14 +34,15 @@ Without jdtls, the server runs in standalone mode — custom rules still work, b
|
|
|
34
34
|
| `catch-rethrow` | catch wraps + rethrows | `Try.of().toEither()` | — |
|
|
35
35
|
| `mutable-variable` | Variable reassignment | Final + functional transforms | — |
|
|
36
36
|
| `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()` | — |
|
|
37
|
-
| `mutable-dto` | `@Data` or `@Setter` | `@Value` (immutable) |
|
|
38
|
-
| `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
|
|
37
|
+
| `mutable-dto` | `@Data` or `@Setter` | `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
|
|
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" }
|
|
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
|
|
76
|
+
The server automatically detects JetBrains IDEs and disables the jdtls proxy — IntelliJ provides its own Java language features (completions, hover, go-to-definition, compile errors). Only the 17 custom functional programming rules run, and they appear alongside IntelliJ's built-in inspections.
|
|
77
77
|
|
|
78
78
|
## Troubleshooting
|
|
79
79
|
|
|
@@ -57,4 +57,4 @@ This extension **coexists** with the Red Hat Java extension (`redhat.java`) and
|
|
|
57
57
|
|
|
58
58
|
## Rules
|
|
59
59
|
|
|
60
|
-
See the [main README](../../README.md) for the full list of
|
|
60
|
+
See the [main README](../../README.md) for the full list of 17 rules and configuration options via `.java-functional-lsp.json`.
|