Skip to main content

java-functional-lsp

CI PyPI version Python License: MIT

A Java Language Server that provides three things in one:

  1. Full Java language support — completions, hover, go-to-definition, compile errors, missing imports — by proxying Eclipse jdtls under the hood
  2. 17 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.

What it checks

Java language (via jdtls)

When jdtls is installed, the server proxies all standard Java language features:

  • Compile errors and warnings
  • Missing imports and unresolved symbols
  • Type mismatches
  • Completions, hover, go-to-definition, find references

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.

Functional programming rules

Rule Detects Suggests Quick Fix
null-literal-arg null passed as method argument Option.none() or default value —
null-return return null Option.of(), Option.none(), or Either ✅
null-assignment Type x = null Option<Type> —
null-field-assignment Field initialized to null Option<T> with Option.none() —
throw-statement throw new XxxException(...) Either.left() or Try.of() —
catch-rethrow catch block that wraps + rethrows Try.of().toEither() —
mutable-variable Local variable reassignment Final variables + functional transforms —
imperative-loop for/while loops .map()/.filter()/.flatMap()/.foldLeft() —
mutable-dto @Data or @Setter on class @Value (immutable); record instead at sourceLevel 16+ ✅
imperative-option-unwrap if (opt.isDefined()) { opt.get() } map()/flatMap()/fold() ✅
field-injection @Autowired on field Constructor injection —
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 / return Either.left instead of throwing —
option-map-nullable Option.map(x -> x.get(k)) followed by chained call (Some(null) risk) .flatMap(x -> Option.of(...)) —

Install

# Homebrew
brew install aviadshiber/tap/java-functional-lsp

# pip
pip install java-functional-lsp

# From source
pip install git+https://github.com/aviadshiber/java-functional-lsp.git

# Optional: install jdtls for full Java language support (see above)
brew install jdtls

Requirements:

  • Python 3.10+ (for the LSP server)
  • JDK 21+ (only if using jdtls — jdtls 1.57+ requires JDK 21 as its runtime, but can analyze Java 8+ source code)

IDE Setup

VS Code

Install the extension from a .vsix file (download from releases):

# Download and install
gh release download --repo aviadshiber/java-functional-lsp --pattern "*.vsix" --dir /tmp
code --install-extension /tmp/java-functional-lsp-*.vsix

Or build from source:

cd editors/vscode
npm install && npm run compile
npx vsce package
code --install-extension java-functional-lsp-*.vsix

The extension is a thin launcher — it just starts the java-functional-lsp binary for .java files. Updating rules only requires upgrading the LSP binary (brew upgrade java-functional-lsp or pip install --upgrade java-functional-lsp). The VSIX itself rarely needs updating.

Configure the binary path in settings if needed (javaFunctionalLsp.serverPath). See editors/vscode/README.md for details.

IntelliJ IDEA

Use the LSP4IJ plugin (works on Community & Ultimate):

  1. Install LSP4IJ from the JetBrains Marketplace
  2. Settings → Languages & Frameworks → Language Servers → +
  3. Set Command: java-functional-lsp, then in Mappings → File name patterns add *.java with Language Id java

The server automatically detects JetBrains IDEs and disables the jdtls proxy (IntelliJ provides native Java support). To force-enable jdtls, set JAVA_FUNCTIONAL_LSP_JDTLS=on in the server command environment.

See editors/intellij/README.md for detailed instructions.

Claude Code

Step 1: Enable LSP support (required, one-time):

Add lspServers to ~/.claude/settings.json (the plugin handles this automatically — only needed for manual setup):

{
  "lspServers": {
    "java-functional": {
      "command": "java-functional-lsp",
      "extensionToLanguage": { ".java": "java" }
    }
  }
}

Step 2: Install the plugin:

claude plugin add https://github.com/aviadshiber/java-functional-lsp.git

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.

Manual hook setup (without the plugin) — add the lint hook to ~/.claude/settings.json, pointing at a checkout of this repo:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|MultiEdit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /path/to/java-functional-lsp/hooks/post_tool_lint.py",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

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.

Or manually add to your Claude Code config:

{
  "lspServers": {
    "java-functional": {
      "command": "java-functional-lsp",
      "extensionToLanguage": { ".java": "java" }
    }
  }
}

Alternative: project-level .lsp.json — instead of installing the plugin or editing global config, add a .lsp.json file at your project root:

{
  "java-functional": {
    "command": "java-functional-lsp",
    "extensionToLanguage": { ".java": "java" }
  }
}

This is useful for CI environments, containers, or ensuring all team members get the LSP server without individual setup. The java-functional-lsp binary must still be installed (pip install java-functional-lsp or brew install aviadshiber/tap/java-functional-lsp).

Step 3: Nudge Claude to prefer LSP (recommended):

Add to ~/.claude/rules/code-intelligence.md:

# Code Intelligence

Prefer LSP over Grep/Glob/Read for code navigation:
- goToDefinition / goToImplementation to jump to source
- findReferences to see all usages across the codebase
- hover for type info without reading the file

After writing or editing code, check LSP diagnostics before
moving on. Fix any type errors or missing imports immediately.

Troubleshooting:

Issue Fix
No diagnostics appear Ensure lspServers is configured (plugin or settings.json), restart
"java-functional-lsp not found" Run brew install aviadshiber/tap/java-functional-lsp
Plugin not active Run claude plugin list to verify, then /reload-plugins
Diagnostics slow on first open Normal — tree-sitter parses on first load, then incremental

Other Editors

Any LSP client that supports stdio transport can use this server. Point it to the java-functional-lsp command for java files.

Editor Config
Neovim vim.lsp.start({ cmd = {"java-functional-lsp"}, filetypes = {"java"} })
Emacs (eglot) (add-to-list 'eglot-server-programs '(java-mode "java-functional-lsp"))
Sublime Text LSP package → add server with "command": ["java-functional-lsp"]

Configuration

Create .java-functional-lsp.json in your project root to customize rules:

{
  "excludes": ["**/generated/**", "**/vendor/**"],
  "sourceLevel": 17,
  "rules": {
    "null-literal-arg": "warning",
    "throw-statement": "info",
    "imperative-loop": "hint",
    "mutable-dto": "off"
  }
}

Options:

  • excludes — glob patterns for files/directories to skip entirely (supports ** for multi-segment wildcards)
  • rules — per-rule severity: error, warning (default), info, hint, off
  • 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).
  • suppressJdtlsPatterns — list of regex patterns to suppress jdtls diagnostics (see below)

Spring-aware behavior:

  • 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

Source-level-aware behavior:

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

Inline suppression with @SuppressWarnings:

// Suppress a specific rule on a method
@SuppressWarnings("java-functional-lsp:null-return")
public String findUser() { return null; }  // no diagnostic

// Suppress multiple rules
@SuppressWarnings({"java-functional-lsp:null-return", "java-functional-lsp:throw-statement"})
public String findUser() { ... }

// Suppress all java-functional-lsp rules
@SuppressWarnings("java-functional-lsp")
public String legacyMethod() { ... }

Works on classes, methods, constructors, fields, and local variables. Suppression applies to the annotated scope — a class-level annotation suppresses all methods within it.

Lombok support

Projects using Lombok need the Lombok Java agent for jdtls to process @Builder, @Value, @Data, @Slf4j, and other annotations. Without it, jdtls reports false "method undefined" errors for generated methods.

The server auto-discovers lombok.jar from these locations (first match wins):

  1. Project config — add to .java-functional-lsp.json:
    { "lombok": "/path/to/lombok.jar" }
    
  2. Environment variable — LOMBOK_JAR=/path/to/lombok.jar
  3. Maven cache — auto-discovered from ~/.m2/repository/org/projectlombok/lombok/
  4. Dedicated directory — ~/.jdtls-libs/lombok.jar

If Lombok is used in your project but the jar isn't found, the server logs a warning.

jdtls settings

The server sends Maven/Gradle settings to jdtls at startup via initializationOptions.settings. Defaults are optimized for Maven monorepos (Maven enabled, Gradle disabled, build artifact exclusions). Override via .java-functional-lsp.json:

{
  "jdtls": {
    "settings": {
      "java": {
        "import": {
          "maven": { "enabled": true },
          "gradle": { "enabled": true },
          "exclusions": ["**/node_modules/**", "**/target/**"]
        },
        "configuration": { "updateBuildConfiguration": "automatic" },
        "maven": { "downloadSources": true }
      }
    }
  }
}

Custom settings fully replace the defaults (no merge). See the jdtls Preferences reference for all available keys.

jdtls cache

The jdtls Eclipse workspace index is cached in ~/.cache/jdtls-data/. Warm starts (~10-20s) reuse this cache; cold starts (60-120s) rebuild from scratch. The cache is automatically invalidated when jdtls or Java is upgraded, but not when java-functional-lsp is upgraded — our Python code changes don't affect the Eclipse index.

To force a clean rebuild: rm -rf ~/.cache/jdtls-data/

Suppressing jdtls diagnostics

For project-specific jdtls false positives (e.g., annotation processor methods, MapStruct mappers), use suppressJdtlsPatterns to add custom regex filters:

{
  "suppressJdtlsPatterns": [
    "The method \\w+Mapper\\(\\) is undefined",
    "cannot be resolved to a type"
  ]
}

Each entry is a regex matched against jdtls diagnostic messages. Invalid patterns are skipped with a warning.

Code actions (quick fixes)

The server provides LSP code actions (textDocument/codeAction) that automatically refactor code. When your editor shows a diagnostic with a lightbulb icon, clicking it applies the fix:

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

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

Agent mode (AI integration)

Every diagnostic includes a machine-readable data payload designed for AI agents like Claude Code:

{
  "code": "frozen-mutation",
  "message": "Runtime Exception Risk: Mutating a frozen structure...",
  "data": {
    "fixType": "REPLACE_WITH_VAVR_LIST",
    "targetLibrary": "io.vavr.collection.List",
    "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability.",
    "recommendedApi": ".append / .appendAll / .update / .remove (returns a new persistent collection)",
    "suggestedSnippet": "list = list.append(\"c\");  // returns a new persistent collection"
  }
}

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.

Agent mode configuration in .java-functional-lsp.json:

{
  "autoImportVavr": true,
  "strictPurity": true
}
Key Default Effect
autoImportVavr true Quick fixes auto-add Vavr/Option imports
autoImportLombok true The mutable-dto quick fix auto-adds import lombok.Value
strictPurity false When true, impure-method uses WARNING severity instead of HINT
sourceLevel (alias javaVersion) 8 Java source level; mutable-dto recommends record over @Value at 16+

Note: The machine-readable data payload is always included in diagnostics when available — no configuration needed.

How it works

The server has two layers:

  • Custom rules — uses tree-sitter with the Java grammar for sub-millisecond AST analysis (~0.4ms per file). No compiler or classpath needed — runs on raw source files.
  • Java language features — proxies Eclipse jdtls for compile errors, completions, hover, go-to-definition, and references. Diagnostics from both layers are merged and published together.

The server speaks the Language Server Protocol (LSP) via stdio, making it compatible with any LSP client.

Development

# Clone and setup
git clone https://github.com/aviadshiber/java-functional-lsp.git
cd java-functional-lsp
uv sync
git config core.hooksPath .githooks

# Run checks
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run mypy src/
uv run pytest

Git hooks in .githooks/ enforce quality automatically:

  • pre-commit — runs lint, format, type check, and tests before each commit
  • pre-push — blocks direct pushes to main (use feature branches + PRs)

See CONTRIBUTING.md for full guidelines.

License

MIT

Metadata

Release files for java-functional-lsp 0.12.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for java-functional-lsp 0.12.0
File Size Uploaded Provenance
java_functional_lsp-0.12.0.tar.gz 254.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for java-functional-lsp 0.12.0
File Interpreter ABI Platform
java_functional_lsp-0.12.0-py3-none-any.whl Python 3 none any Details

Total release size: 347.2 kB

Release files / java_functional_lsp-0.12.0.tar.gz

Download URL java_functional_lsp-0.12.0.tar.gz
Size 254.6 kB
Tags Source
SHA-256 checksum
How to use checksums
eb7da7e9f5093b953bb96f7ff252c077bdf85f37a5af68f397c36ddfeb9b69be
BLAKE2b-256 checksum
How to use checksums
e53b1935623fc87d527542049156c122bc0312546c5e358439a5034fded9a8c2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / java_functional_lsp-0.12.0-py3-none-any.whl

Download URL java_functional_lsp-0.12.0-py3-none-any.whl
Size 92.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1bda4de143d8895cdc5a68bb4cc1acfb413c5d7f4ffa93c2dc969fa7b7a9918b
BLAKE2b-256 checksum
How to use checksums
afc5c05c9b4e7cf470dbe3782d0622872d9f770ff9ff60c75ee436d3ab35d900
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Security for java-functional-lsp 0.12.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for java_functional_lsp-0.12.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.12.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page