java-functional-lsp 0.7.1__tar.gz → 0.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- java_functional_lsp-0.7.3/.github/workflows/test.yml +109 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/CONTRIBUTING.md +12 -1
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/PKG-INFO +7 -5
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/README.md +6 -4
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/SKILL.md +7 -5
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/pyproject.toml +5 -2
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/server.py +98 -18
- java_functional_lsp-0.7.3/tests/test_e2e_jdtls.py +451 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_proxy.py +141 -0
- java_functional_lsp-0.7.3/tests/test_server.py +672 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/uv.lock +1 -1
- java_functional_lsp-0.7.1/.github/workflows/test.yml +0 -36
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/.gitignore +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/LICENSE +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/base.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/fixes.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/src/java_functional_lsp/proxy.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/__init__.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/conftest.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_base.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_config.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_e2e.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_exception_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_fixes.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_functional_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_mutation_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.7.1 → java_functional_lsp-0.7.3}/tests/test_suppress.py +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest]
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v7
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: uv run ruff check src/ tests/
|
|
29
|
+
- name: Format check
|
|
30
|
+
run: uv run ruff format --check src/ tests/
|
|
31
|
+
- name: Type check
|
|
32
|
+
run: uv run mypy src/
|
|
33
|
+
- name: Test
|
|
34
|
+
run: uv run pytest -q -m "not e2e"
|
|
35
|
+
env:
|
|
36
|
+
NO_COLOR: "1"
|
|
37
|
+
|
|
38
|
+
integration:
|
|
39
|
+
name: Integration (with jdtls) / ${{ matrix.os }}
|
|
40
|
+
runs-on: ${{ matrix.os }}
|
|
41
|
+
strategy:
|
|
42
|
+
fail-fast: false
|
|
43
|
+
matrix:
|
|
44
|
+
os: [ubuntu-latest, macos-latest]
|
|
45
|
+
# Runs the FULL test suite (unit + e2e) with jdtls installed, on one
|
|
46
|
+
# Python version per OS. This is a comprehensive sanity check that
|
|
47
|
+
# exercises: custom analyzer diagnostics, code action generation,
|
|
48
|
+
# AND jdtls request forwarding end-to-end. The unit matrix above
|
|
49
|
+
# provides fast Python-version-specific feedback without jdtls.
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v6
|
|
52
|
+
- name: Set up Python 3.12
|
|
53
|
+
uses: actions/setup-python@v6
|
|
54
|
+
with:
|
|
55
|
+
python-version: "3.12"
|
|
56
|
+
- name: Install Java 21
|
|
57
|
+
uses: actions/setup-java@v5
|
|
58
|
+
with:
|
|
59
|
+
distribution: temurin
|
|
60
|
+
java-version: "21"
|
|
61
|
+
- name: Install jdtls (macOS)
|
|
62
|
+
if: runner.os == 'macOS'
|
|
63
|
+
run: brew install jdtls
|
|
64
|
+
- name: Install jdtls (Linux)
|
|
65
|
+
if: runner.os == 'Linux'
|
|
66
|
+
run: |
|
|
67
|
+
set -euo pipefail
|
|
68
|
+
# Download the Eclipse JDT Language Server milestone build directly.
|
|
69
|
+
# Pinned to match the Homebrew formula so Linux + macOS exercise the
|
|
70
|
+
# same version. When bumping, update ALL THREE of JDTLS_VERSION,
|
|
71
|
+
# JDTLS_BUILD, and JDTLS_SHA256 together — the canonical source is
|
|
72
|
+
# the Homebrew formula:
|
|
73
|
+
# https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/j/jdtls.rb
|
|
74
|
+
# It has `url "...jdt-language-server-<version>-<build>.tar.gz"`
|
|
75
|
+
# and `sha256 "<hex>"` which you can copy verbatim.
|
|
76
|
+
JDTLS_VERSION="1.57.0"
|
|
77
|
+
JDTLS_BUILD="202602261110"
|
|
78
|
+
JDTLS_SHA256="f7ffa93fe1bbbea95dac13dd97cdcd25c582d6e56db67258da0dcceb2302601e"
|
|
79
|
+
JDTLS_URL="https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/${JDTLS_VERSION}/jdt-language-server-${JDTLS_VERSION}-${JDTLS_BUILD}.tar.gz&r=1"
|
|
80
|
+
JDTLS_DIR="$HOME/.local/share/jdtls"
|
|
81
|
+
BIN_DIR="$HOME/.local/bin"
|
|
82
|
+
mkdir -p "$JDTLS_DIR" "$BIN_DIR"
|
|
83
|
+
# -L follows Eclipse's mirror redirect; -f fails loudly on 404.
|
|
84
|
+
curl -sSLf -o /tmp/jdtls.tar.gz "$JDTLS_URL"
|
|
85
|
+
# Verify the tarball integrity against the hash pinned in the
|
|
86
|
+
# Homebrew formula. This protects against mirror tampering —
|
|
87
|
+
# without it, a compromised Eclipse mirror could ship arbitrary
|
|
88
|
+
# code that our e2e tests would then execute.
|
|
89
|
+
echo "${JDTLS_SHA256} /tmp/jdtls.tar.gz" | sha256sum -c -
|
|
90
|
+
tar -xzf /tmp/jdtls.tar.gz -C "$JDTLS_DIR"
|
|
91
|
+
# Wrapper script on PATH that invokes the bundled Python launcher
|
|
92
|
+
# with python3 (the tarball ships jdtls.py in bin/).
|
|
93
|
+
printf '#!/bin/bash\nexec python3 "%s/bin/jdtls" "$@"\n' "$JDTLS_DIR" > "$BIN_DIR/jdtls"
|
|
94
|
+
chmod +x "$BIN_DIR/jdtls"
|
|
95
|
+
echo "$BIN_DIR" >> "$GITHUB_PATH"
|
|
96
|
+
- name: Install uv
|
|
97
|
+
uses: astral-sh/setup-uv@v7
|
|
98
|
+
- name: Install dependencies
|
|
99
|
+
run: uv sync
|
|
100
|
+
- name: Verify jdtls is available
|
|
101
|
+
run: |
|
|
102
|
+
which jdtls
|
|
103
|
+
jdtls --help | head -5 || true
|
|
104
|
+
java -version
|
|
105
|
+
echo "JAVA_HOME=$JAVA_HOME"
|
|
106
|
+
- name: Run full test suite (unit + e2e)
|
|
107
|
+
run: uv run pytest -v
|
|
108
|
+
env:
|
|
109
|
+
NO_COLOR: "1"
|
|
@@ -48,9 +48,20 @@ uv run pytest
|
|
|
48
48
|
4. Add a `DiagnosticData` entry to the module's `_DATA` dict with `fix_type`, `target_library`, and `rationale`
|
|
49
49
|
5. Pass `data=_DATA["rule-id"]` when creating the `Diagnostic`
|
|
50
50
|
6. Add tests in `tests/test_<analyzer>.py` (including a test verifying the `data` field)
|
|
51
|
-
7. Optionally add a quick fix generator in `src/java_functional_lsp/fixes.py` and register it in `_FIX_REGISTRY`
|
|
51
|
+
7. Optionally add a quick fix generator in `src/java_functional_lsp/fixes.py` and register it in `_FIX_REGISTRY` + add its title to `_FIX_TITLES` in `server.py` (an import-time assertion catches mismatches)
|
|
52
52
|
8. Update the rules table in `README.md`
|
|
53
53
|
|
|
54
|
+
## Test Architecture
|
|
55
|
+
|
|
56
|
+
The project has a layered test suite:
|
|
57
|
+
|
|
58
|
+
- **Unit tests** (`tests/test_*_checker.py`, `tests/test_fixes.py`, `tests/test_proxy.py`) — fast, focused, run in the main CI matrix across Python 3.10-3.13 on Ubuntu + macOS
|
|
59
|
+
- **Server integration tests** (`tests/test_server.py: TestServerInternals`) — exercise the server pipeline (config loading, diagnostic conversion, code actions) in-process
|
|
60
|
+
- **LSP lifecycle tests** (`tests/test_server.py: TestLspLifecycle`) — **zero mocks** — spawn the real server as a subprocess via pygls `LanguageClient`, connect over stdio, exercise the full LSP round-trip (initialize, didOpen, publishDiagnostics, codeAction, didChange)
|
|
61
|
+
- **jdtls e2e tests** (`tests/test_e2e_jdtls.py`) — **zero mocks** — spawn real jdtls, exercise definition/references/hover/completion/documentSymbol forwarding. Auto-skip when jdtls is not installed. Run in a dedicated CI integration job.
|
|
62
|
+
|
|
63
|
+
Coverage threshold is **80%**. Bump the version in both `pyproject.toml` and `src/java_functional_lsp/__init__.py` when making source changes (a pre-commit hook enforces this).
|
|
64
|
+
|
|
54
65
|
## Reporting Issues
|
|
55
66
|
|
|
56
67
|
- Use the [bug report template](https://github.com/aviadshiber/java-functional-lsp/issues/new?template=bug-report.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-functional-lsp
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
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 two 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. **16 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+). 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 16 custom rules still work, but you won't get compile errors or completions.
|
|
56
56
|
|
|
57
57
|
### Functional programming rules
|
|
58
58
|
|
|
@@ -72,6 +72,7 @@ Install jdtls separately: `brew install jdtls` (requires JDK 21+). Without jdtls
|
|
|
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
|
+
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
|
|
75
76
|
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
|
|
76
77
|
|
|
77
78
|
## Install
|
|
@@ -231,7 +232,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
231
232
|
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
232
233
|
|
|
233
234
|
**Spring-aware behavior:**
|
|
234
|
-
- `throw-statement` and `catch-
|
|
235
|
+
- `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
|
|
235
236
|
- `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
|
|
236
237
|
|
|
237
238
|
**Inline suppression** with `@SuppressWarnings`:
|
|
@@ -259,8 +260,9 @@ The server provides LSP code actions (`textDocument/codeAction`) that automatica
|
|
|
259
260
|
| Rule | Code Action | What it does |
|
|
260
261
|
|------|-------------|--------------|
|
|
261
262
|
| `frozen-mutation` | Switch to Vavr Immutable Collection | Rewrites `List.of()` → `io.vavr.collection.List.of()`, `.add(x)` → `= list.append(x)`, adds import |
|
|
262
|
-
| `null-check-to-monadic` | Convert to Option monadic flow | Rewrites `if (x != null) { return x.foo(); }` → `Option.of(x).map(...)
|
|
263
|
+
| `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 |
|
|
263
264
|
| `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
|
|
265
|
+
| `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. |
|
|
264
266
|
|
|
265
267
|
Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
|
|
266
268
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
A Java Language Server that provides two 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. **16 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+). 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 16 custom rules still work, but you won't get compile errors or completions.
|
|
28
28
|
|
|
29
29
|
### Functional programming rules
|
|
30
30
|
|
|
@@ -44,6 +44,7 @@ Install jdtls separately: `brew install jdtls` (requires JDK 21+). Without jdtls
|
|
|
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
|
+
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
|
|
47
48
|
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
|
|
48
49
|
|
|
49
50
|
## Install
|
|
@@ -203,7 +204,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
203
204
|
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
204
205
|
|
|
205
206
|
**Spring-aware behavior:**
|
|
206
|
-
- `throw-statement` and `catch-
|
|
207
|
+
- `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
|
|
207
208
|
- `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
|
|
208
209
|
|
|
209
210
|
**Inline suppression** with `@SuppressWarnings`:
|
|
@@ -231,8 +232,9 @@ The server provides LSP code actions (`textDocument/codeAction`) that automatica
|
|
|
231
232
|
| Rule | Code Action | What it does |
|
|
232
233
|
|------|-------------|--------------|
|
|
233
234
|
| `frozen-mutation` | Switch to Vavr Immutable Collection | Rewrites `List.of()` → `io.vavr.collection.List.of()`, `.add(x)` → `= list.append(x)`, adds import |
|
|
234
|
-
| `null-check-to-monadic` | Convert to Option monadic flow | Rewrites `if (x != null) { return x.foo(); }` → `Option.of(x).map(...)
|
|
235
|
+
| `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 |
|
|
235
236
|
| `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
|
|
237
|
+
| `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. |
|
|
236
238
|
|
|
237
239
|
Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
|
|
238
240
|
|
|
@@ -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 16 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 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.
|
|
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 (16 checks)
|
|
26
26
|
|
|
27
27
|
| Rule | Detects | Suggests | Quick Fix |
|
|
28
28
|
|------|---------|----------|-----------|
|
|
@@ -40,6 +40,7 @@ Without jdtls, the server runs in standalone mode — custom rules still work, b
|
|
|
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
|
+
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
|
|
43
44
|
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
|
|
44
45
|
|
|
45
46
|
## Code Actions (Quick Fixes)
|
|
@@ -47,8 +48,9 @@ Without jdtls, the server runs in standalone mode — custom rules still work, b
|
|
|
47
48
|
Rules marked ✅ provide automated `textDocument/codeAction` fixes:
|
|
48
49
|
|
|
49
50
|
- **frozen-mutation** → "Switch to Vavr Immutable Collection" — rewrites type, init, and mutation call to Vavr persistent API, adds import
|
|
50
|
-
- **null-check-to-monadic** → "Convert to Option monadic flow" — rewrites `if (x != null)` to `Option.of(x).map(...)`, adds import
|
|
51
|
+
- **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
|
|
51
52
|
- **null-return** → "Replace with Option.none()" — replaces `null` with `Option.none()`, adds import
|
|
53
|
+
- **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.
|
|
52
54
|
|
|
53
55
|
## Agent-Ready Diagnostics
|
|
54
56
|
|
|
@@ -85,7 +87,7 @@ Create `.java-functional-lsp.json` in your project root:
|
|
|
85
87
|
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
86
88
|
- `autoImportVavr` — quick fixes auto-add Vavr imports (default: `true`)
|
|
87
89
|
- `strictPurity` — `impure-method` uses WARNING instead of HINT (default: `false`)
|
|
88
|
-
- `throw-statement`/`catch-rethrow` auto-suppressed in `@Bean` methods
|
|
90
|
+
- `throw-statement`/`catch-rethrow`/`try-catch-to-monadic` auto-suppressed in `@Bean` methods
|
|
89
91
|
- `mutable-dto` suggests `@ConstructorBinding` for `@ConfigurationProperties` classes
|
|
90
92
|
- Inline suppression: `@SuppressWarnings("java-functional-lsp:rule-id")` on any declaration
|
|
91
93
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "java-functional-lsp"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.3"
|
|
8
8
|
description = "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -63,8 +63,11 @@ testpaths = ["tests"]
|
|
|
63
63
|
python_files = ["test_*.py"]
|
|
64
64
|
python_classes = ["Test*"]
|
|
65
65
|
python_functions = ["test_*"]
|
|
66
|
-
addopts = "--cov=java_functional_lsp --cov-report=term-missing --cov-fail-under=
|
|
66
|
+
addopts = "--cov=java_functional_lsp --cov-report=term-missing --cov-fail-under=80"
|
|
67
67
|
asyncio_mode = "auto"
|
|
68
|
+
markers = [
|
|
69
|
+
"e2e: end-to-end tests that spawn a real jdtls subprocess (require jdtls + Java 21+; skipped when unavailable)",
|
|
70
|
+
]
|
|
68
71
|
|
|
69
72
|
[tool.ruff]
|
|
70
73
|
line-length = 120
|
|
@@ -13,8 +13,8 @@ import sys
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
-
import cattrs
|
|
17
16
|
from lsprotocol import types as lsp
|
|
17
|
+
from lsprotocol.converters import get_converter
|
|
18
18
|
from pygls.lsp.server import LanguageServer
|
|
19
19
|
from pygls.uris import to_fs_path
|
|
20
20
|
|
|
@@ -45,7 +45,14 @@ _ANALYZERS: list[Analyzer] = [
|
|
|
45
45
|
FunctionalChecker(),
|
|
46
46
|
]
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
#: LSP-aware cattrs converter. Unstructures to the LSP JSON shape
|
|
49
|
+
#: (camelCase field names, discriminated unions, None-field pruning) and
|
|
50
|
+
#: correspondingly structures from the same shape. Using a vanilla
|
|
51
|
+
#: ``cattrs.Converter()`` here emits snake_case field names (``text_document``
|
|
52
|
+
#: instead of ``textDocument``), which breaks request forwarding to jdtls —
|
|
53
|
+
#: jdtls then sees a null ``TextDocumentIdentifier`` and throws NPEs during
|
|
54
|
+
#: go-to-definition, references, etc.
|
|
55
|
+
_converter = get_converter()
|
|
49
56
|
|
|
50
57
|
|
|
51
58
|
class JavaFunctionalLspServer(LanguageServer):
|
|
@@ -221,11 +228,11 @@ def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|
|
221
228
|
change=lsp.TextDocumentSyncKind.Full,
|
|
222
229
|
save=lsp.SaveOptions(include_text=True),
|
|
223
230
|
),
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
231
|
+
# Only advertise capabilities we own (custom diagnostics + code actions).
|
|
232
|
+
# jdtls-dependent features (hover, definition, references, completion,
|
|
233
|
+
# documentSymbol) are registered dynamically after jdtls starts — see
|
|
234
|
+
# on_initialized(). This prevents us from claiming hover when jdtls
|
|
235
|
+
# isn't ready, which would suppress the IDE's diagnostic tooltips.
|
|
229
236
|
code_action_provider=lsp.CodeActionOptions(
|
|
230
237
|
code_action_kinds=[lsp.CodeActionKind.QuickFix],
|
|
231
238
|
),
|
|
@@ -243,10 +250,71 @@ async def on_initialized(params: lsp.InitializedParams) -> None:
|
|
|
243
250
|
started = await server._proxy.start(server._init_params)
|
|
244
251
|
if started:
|
|
245
252
|
logger.info("jdtls proxy active — full Java language support enabled")
|
|
253
|
+
await _register_jdtls_capabilities()
|
|
246
254
|
else:
|
|
247
255
|
logger.info("jdtls proxy unavailable — running with custom rules only")
|
|
248
256
|
|
|
249
257
|
|
|
258
|
+
_JAVA_SELECTOR = [lsp.TextDocumentFilterLanguage(language="java")]
|
|
259
|
+
|
|
260
|
+
_JDTLS_REG_PREFIX = "jdtls-"
|
|
261
|
+
|
|
262
|
+
# jdtls-dependent capabilities registered dynamically after the proxy starts.
|
|
263
|
+
# Each entry: (id_suffix, LSP method, registration options class, extra kwargs).
|
|
264
|
+
_JDTLS_CAPABILITIES: list[tuple[str, str, type[Any], dict[str, Any]]] = [
|
|
265
|
+
("completion", lsp.TEXT_DOCUMENT_COMPLETION, lsp.CompletionRegistrationOptions, {"trigger_characters": ["."]}),
|
|
266
|
+
("hover", lsp.TEXT_DOCUMENT_HOVER, lsp.HoverRegistrationOptions, {}),
|
|
267
|
+
("definition", lsp.TEXT_DOCUMENT_DEFINITION, lsp.DefinitionRegistrationOptions, {}),
|
|
268
|
+
("references", lsp.TEXT_DOCUMENT_REFERENCES, lsp.ReferenceRegistrationOptions, {}),
|
|
269
|
+
("document-symbol", lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL, lsp.DocumentSymbolRegistrationOptions, {}),
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
# Maps LSP method → handler function for dynamic registration.
|
|
273
|
+
_JDTLS_HANDLERS: dict[str, Any] = {}
|
|
274
|
+
|
|
275
|
+
# Set after first successful registration to prevent FeatureAlreadyRegisteredError.
|
|
276
|
+
_jdtls_capabilities_registered = False
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _build_jdtls_registrations() -> list[lsp.Registration]:
|
|
280
|
+
"""Build LSP Registration objects for jdtls-dependent capabilities."""
|
|
281
|
+
return [
|
|
282
|
+
lsp.Registration(
|
|
283
|
+
id=f"{_JDTLS_REG_PREFIX}{suffix}",
|
|
284
|
+
method=method,
|
|
285
|
+
register_options=_converter.unstructure(opts_cls(document_selector=_JAVA_SELECTOR, **extra)),
|
|
286
|
+
)
|
|
287
|
+
for suffix, method, opts_cls, extra in _JDTLS_CAPABILITIES
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
async def _register_jdtls_capabilities() -> None:
|
|
292
|
+
"""Dynamically register jdtls-dependent capabilities after the proxy starts.
|
|
293
|
+
|
|
294
|
+
We don't advertise these in the static InitializeResult because doing so
|
|
295
|
+
would make the IDE defer hover/definition/etc to us even before jdtls is
|
|
296
|
+
ready, which suppresses the IDE's built-in diagnostic tooltips.
|
|
297
|
+
|
|
298
|
+
Idempotent: safe to call multiple times (e.g., proxy restart).
|
|
299
|
+
"""
|
|
300
|
+
global _jdtls_capabilities_registered
|
|
301
|
+
if _jdtls_capabilities_registered:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
# Register handlers so pygls dispatches incoming requests to them.
|
|
306
|
+
for method, handler in _JDTLS_HANDLERS.items():
|
|
307
|
+
server.feature(method)(handler)
|
|
308
|
+
|
|
309
|
+
# Tell the client we now support these capabilities.
|
|
310
|
+
registrations = _build_jdtls_registrations()
|
|
311
|
+
await server.client_register_capability_async(lsp.RegistrationParams(registrations=registrations))
|
|
312
|
+
_jdtls_capabilities_registered = True
|
|
313
|
+
logger.info("Dynamically registered jdtls capabilities (hover, definition, references, completion, symbol)")
|
|
314
|
+
except Exception:
|
|
315
|
+
logger.warning("Failed to dynamically register jdtls capabilities", exc_info=True)
|
|
316
|
+
|
|
317
|
+
|
|
250
318
|
# --- Document sync (forward to jdtls + run custom analyzers) ---
|
|
251
319
|
|
|
252
320
|
|
|
@@ -304,11 +372,15 @@ async def on_did_close(params: lsp.DidCloseTextDocumentParams) -> None:
|
|
|
304
372
|
await server._proxy.send_notification("textDocument/didClose", _serialize_params(params))
|
|
305
373
|
|
|
306
374
|
|
|
307
|
-
# ---
|
|
375
|
+
# --- jdtls passthrough handlers (registered dynamically, NOT at module level) ---
|
|
376
|
+
#
|
|
377
|
+
# These are NOT decorated with @server.feature because pygls auto-advertises
|
|
378
|
+
# capabilities for decorated handlers. Instead, they are collected in
|
|
379
|
+
# _JDTLS_HANDLERS and registered inside _register_jdtls_capabilities() so
|
|
380
|
+
# they only activate after jdtls starts.
|
|
308
381
|
|
|
309
382
|
|
|
310
|
-
|
|
311
|
-
async def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | None:
|
|
383
|
+
async def _on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | None:
|
|
312
384
|
"""Forward completion request to jdtls."""
|
|
313
385
|
if not server._proxy.is_available:
|
|
314
386
|
return None
|
|
@@ -321,8 +393,7 @@ async def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList | No
|
|
|
321
393
|
return None
|
|
322
394
|
|
|
323
395
|
|
|
324
|
-
|
|
325
|
-
async def on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
|
|
396
|
+
async def _on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
|
|
326
397
|
"""Forward hover request to jdtls."""
|
|
327
398
|
if not server._proxy.is_available:
|
|
328
399
|
return None
|
|
@@ -335,8 +406,7 @@ async def on_hover(params: lsp.HoverParams) -> lsp.Hover | None:
|
|
|
335
406
|
return None
|
|
336
407
|
|
|
337
408
|
|
|
338
|
-
|
|
339
|
-
async def on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | None:
|
|
409
|
+
async def _on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | None:
|
|
340
410
|
"""Forward go-to-definition request to jdtls."""
|
|
341
411
|
if not server._proxy.is_available:
|
|
342
412
|
return None
|
|
@@ -351,8 +421,7 @@ async def on_definition(params: lsp.DefinitionParams) -> list[lsp.Location] | No
|
|
|
351
421
|
return None
|
|
352
422
|
|
|
353
423
|
|
|
354
|
-
|
|
355
|
-
async def on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | None:
|
|
424
|
+
async def _on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | None:
|
|
356
425
|
"""Forward find-references request to jdtls."""
|
|
357
426
|
if not server._proxy.is_available:
|
|
358
427
|
return None
|
|
@@ -365,8 +434,7 @@ async def on_references(params: lsp.ReferenceParams) -> list[lsp.Location] | Non
|
|
|
365
434
|
return None
|
|
366
435
|
|
|
367
436
|
|
|
368
|
-
|
|
369
|
-
async def on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.DocumentSymbol] | None:
|
|
437
|
+
async def _on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.DocumentSymbol] | None:
|
|
370
438
|
"""Forward document symbol request to jdtls."""
|
|
371
439
|
if not server._proxy.is_available:
|
|
372
440
|
return None
|
|
@@ -379,6 +447,18 @@ async def on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.Docum
|
|
|
379
447
|
return None
|
|
380
448
|
|
|
381
449
|
|
|
450
|
+
# Populate handler map for dynamic registration.
|
|
451
|
+
_JDTLS_HANDLERS.update(
|
|
452
|
+
{
|
|
453
|
+
lsp.TEXT_DOCUMENT_COMPLETION: _on_completion,
|
|
454
|
+
lsp.TEXT_DOCUMENT_HOVER: _on_hover,
|
|
455
|
+
lsp.TEXT_DOCUMENT_DEFINITION: _on_definition,
|
|
456
|
+
lsp.TEXT_DOCUMENT_REFERENCES: _on_references,
|
|
457
|
+
lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL: _on_document_symbol,
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
382
462
|
# --- Code actions (quick fixes) ---
|
|
383
463
|
|
|
384
464
|
# Human-readable titles for code actions
|