Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,79 @@
- name: Type check
run: uv run mypy src/
- name: Test
run: uv run pytest -q
run: uv run pytest -q -m "not e2e"
env:
NO_COLOR: "1"

integration:
name: Integration (with jdtls) / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
# Runs the FULL test suite (unit + e2e) with jdtls installed, on one
# Python version per OS. This is a comprehensive sanity check that
# exercises: custom analyzer diagnostics, code action generation,
# AND jdtls request forwarding end-to-end. The unit matrix above
# provides fast Python-version-specific feedback without jdtls.
steps:
- uses: actions/checkout@v6
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install Java 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"
- name: Install jdtls (macOS)
if: runner.os == 'macOS'
run: brew install jdtls
- name: Install jdtls (Linux)
if: runner.os == 'Linux'
run: |
set -euo pipefail
# Download the Eclipse JDT Language Server milestone build directly.
# Pinned to match the Homebrew formula so Linux + macOS exercise the
# same version. When bumping, update ALL THREE of JDTLS_VERSION,
# JDTLS_BUILD, and JDTLS_SHA256 together — the canonical source is
# the Homebrew formula:
# https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/j/jdtls.rb
# It has `url "...jdt-language-server-<version>-<build>.tar.gz"`
# and `sha256 "<hex>"` which you can copy verbatim.
JDTLS_VERSION="1.57.0"
JDTLS_BUILD="202602261110"
JDTLS_SHA256="f7ffa93fe1bbbea95dac13dd97cdcd25c582d6e56db67258da0dcceb2302601e"
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"
JDTLS_DIR="$HOME/.local/share/jdtls"
BIN_DIR="$HOME/.local/bin"
mkdir -p "$JDTLS_DIR" "$BIN_DIR"
# -L follows Eclipse's mirror redirect; -f fails loudly on 404.
curl -sSLf -o /tmp/jdtls.tar.gz "$JDTLS_URL"
# Verify the tarball integrity against the hash pinned in the
# Homebrew formula. This protects against mirror tampering —
# without it, a compromised Eclipse mirror could ship arbitrary
# code that our e2e tests would then execute.
echo "${JDTLS_SHA256} /tmp/jdtls.tar.gz" | sha256sum -c -
tar -xzf /tmp/jdtls.tar.gz -C "$JDTLS_DIR"
# Wrapper script on PATH that invokes the bundled Python launcher
# with python3 (the tarball ships jdtls.py in bin/).
printf '#!/bin/bash\nexec python3 "%s/bin/jdtls" "$@"\n' "$JDTLS_DIR" > "$BIN_DIR/jdtls"
chmod +x "$BIN_DIR/jdtls"
echo "$BIN_DIR" >> "$GITHUB_PATH"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync
- name: Verify jdtls is available
run: |
which jdtls
jdtls --help | head -5 || true
java -version
echo "JAVA_HOME=$JAVA_HOME"
- name: Run full test suite (unit + e2e)
run: uv run pytest -v
env:
NO_COLOR: "1"

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium test

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,20 @@ uv run pytest
4. Add a `DiagnosticData` entry to the module's `_DATA` dict with `fix_type`, `target_library`, and `rationale`
5. Pass `data=_DATA["rule-id"]` when creating the `Diagnostic`
6. Add tests in `tests/test_<analyzer>.py` (including a test verifying the `data` field)
7. Optionally add a quick fix generator in `src/java_functional_lsp/fixes.py` and register it in `_FIX_REGISTRY`
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)
8. Update the rules table in `README.md`

## Test Architecture

The project has a layered test suite:

- **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
- **Server integration tests** (`tests/test_server.py: TestServerInternals`) — exercise the server pipeline (config loading, diagnostic conversion, code actions) in-process
- **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)
- **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.

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).

## Reporting Issues

- Use the [bug report template](https://github.com/aviadshiber/java-functional-lsp/issues/new?template=bug-report.md)
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
A Java Language Server that provides two things in one:

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
2. **15 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
2. **16 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
3. **Code actions (quick fixes)** — automated refactoring via LSP `textDocument/codeAction`, with machine-readable diagnostic metadata for AI agents

Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
Expand All @@ -24,7 +24,7 @@ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the
- Type mismatches
- Completions, hover, go-to-definition, find references

Install jdtls separately: `brew install jdtls` (requires JDK 21+). Without jdtls, the server runs in standalone mode — the 12 custom rules still work, but you won't get compile errors or completions.
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.

### Functional programming rules

Expand All @@ -44,6 +44,7 @@ Install jdtls separately: `brew install jdtls` (requires JDK 21+). Without jdtls
| `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
| `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
| `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |

## Install
Expand Down Expand Up @@ -203,7 +204,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`

**Spring-aware behavior:**
- `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
- `throw-statement`, `catch-rethrow`, and `try-catch-to-monadic` are automatically suppressed inside `@Bean` methods
- `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`

**Inline suppression** with `@SuppressWarnings`:
Expand Down Expand Up @@ -231,8 +232,9 @@ The server provides LSP code actions (`textDocument/codeAction`) that automatica
| Rule | Code Action | What it does |
|------|-------------|--------------|
| `frozen-mutation` | Switch to Vavr Immutable Collection | Rewrites `List.of()` → `io.vavr.collection.List.of()`, `.add(x)` → `= list.append(x)`, adds import |
| `null-check-to-monadic` | Convert to Option monadic flow | Rewrites `if (x != null) { return x.foo(); }` → `Option.of(x).map(...).getOrNull()`, adds import |
| `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 |
| `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
| `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. |

Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.

Expand Down
12 changes: 7 additions & 5 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
name: java-functional-lsp
description: Java LSP with full language support (completions, hover, go-to-def, compile errors) plus 15 functional programming rules with automated quick fixes. Auto-invoke when setting up Java language support or discussing Java linting configuration.
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.
allowed-tools: Bash
disable-model-invocation: true
---

# Java Functional LSP

A Java LSP server that wraps jdtls and adds 15 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.
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.

## Prerequisites

Expand All @@ -22,7 +22,7 @@ brew install jdtls

Without jdtls, the server runs in standalone mode — custom rules still work, but no completions/hover/compile errors.

## Rules (15 checks)
## Rules (16 checks)

| Rule | Detects | Suggests | Quick Fix |
|------|---------|----------|-----------|
Expand All @@ -40,15 +40,17 @@ Without jdtls, the server runs in standalone mode — custom rules still work, b
| `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
| `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
| `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
| `try-catch-to-monadic` | `try { return x(); } catch (E e) { return d; }` | `Try.of(() -> x()).getOrElse(d)` | ✅ |
| `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |

## Code Actions (Quick Fixes)

Rules marked ✅ provide automated `textDocument/codeAction` fixes:

- **frozen-mutation** → "Switch to Vavr Immutable Collection" — rewrites type, init, and mutation call to Vavr persistent API, adds import
- **null-check-to-monadic** → "Convert to Option monadic flow" — rewrites `if (x != null)` to `Option.of(x).map(...)`, adds import
- **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
- **null-return** → "Replace with Option.none()" — replaces `null` with `Option.none()`, adds import
- **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.

## Agent-Ready Diagnostics

Expand Down Expand Up @@ -85,7 +87,7 @@ Create `.java-functional-lsp.json` in your project root:
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
- `autoImportVavr` — quick fixes auto-add Vavr imports (default: `true`)
- `strictPurity` — `impure-method` uses WARNING instead of HINT (default: `false`)
- `throw-statement`/`catch-rethrow` auto-suppressed in `@Bean` methods
- `throw-statement`/`catch-rethrow`/`try-catch-to-monadic` auto-suppressed in `@Bean` methods
- `mutable-dto` suggests `@ConstructorBinding` for `@ConfigurationProperties` classes
- Inline suppression: `@SuppressWarnings("java-functional-lsp:rule-id")` on any declaration

Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "java-functional-lsp"
version = "0.7.1"
version = "0.7.2"
description = "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions"
readme = "README.md"
license = { text = "MIT" }
Expand Down Expand Up @@ -63,8 +63,11 @@ testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "--cov=java_functional_lsp --cov-report=term-missing --cov-fail-under=60"
addopts = "--cov=java_functional_lsp --cov-report=term-missing --cov-fail-under=80"
asyncio_mode = "auto"
markers = [
"e2e: end-to-end tests that spawn a real jdtls subprocess (require jdtls + Java 21+; skipped when unavailable)",
]

[tool.ruff]
line-length = 120
Expand Down
2 changes: 1 addition & 1 deletion src/java_functional_lsp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""java-functional-lsp: A Java LSP server enforcing functional programming best practices."""

__version__ = "0.7.1"
__version__ = "0.7.2"
11 changes: 9 additions & 2 deletions src/java_functional_lsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
from pathlib import Path
from typing import Any

import cattrs
from lsprotocol import types as lsp
from lsprotocol.converters import get_converter
from pygls.lsp.server import LanguageServer
from pygls.uris import to_fs_path

Expand Down Expand Up @@ -45,7 +45,14 @@
FunctionalChecker(),
]

_converter = cattrs.Converter()
#: LSP-aware cattrs converter. Unstructures to the LSP JSON shape
#: (camelCase field names, discriminated unions, None-field pruning) and
#: correspondingly structures from the same shape. Using a vanilla
#: ``cattrs.Converter()`` here emits snake_case field names (``text_document``
#: instead of ``textDocument``), which breaks request forwarding to jdtls —
#: jdtls then sees a null ``TextDocumentIdentifier`` and throws NPEs during
#: go-to-definition, references, etc.
_converter = get_converter()


class JavaFunctionalLspServer(LanguageServer):
Expand Down
Loading
Loading