Skip to main content

Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions

Project description

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

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

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. SettingsLanguages & FrameworksLanguage Servers+
  3. Set Command: java-functional-lsp, then in MappingsFile name patterns add *.java with Language Id java

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

Claude Code

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

Add to ~/.claude/settings.json:

{
  "env": {
    "ENABLE_LSP_TOOL": "1"
  }
}

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 reminds Claude to fix violations on every .java file edit, and the /lint-java command.

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 ENABLE_LSP_TOOL=1 is set, restart Claude Code
"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/**"],
  "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

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

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.

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

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."
  }
}

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.

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

{
  "autoImportVavr": true,
  "strictPurity": true
}
Key Default Effect
autoImportVavr true Quick fixes auto-add Vavr/Option imports
strictPurity false When true, impure-method uses WARNING severity instead of HINT

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

java_functional_lsp-0.7.8.tar.gz (178.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

java_functional_lsp-0.7.8-py3-none-any.whl (53.1 kB view details)

Uploaded Python 3

File details

Details for the file java_functional_lsp-0.7.8.tar.gz.

File metadata

  • Download URL: java_functional_lsp-0.7.8.tar.gz
  • Upload date:
  • Size: 178.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for java_functional_lsp-0.7.8.tar.gz
Algorithm Hash digest
SHA256 f1c0d3a0540f803ced4ed4aaf16c241fac4b0a46c4e009e8ec7753f661d774f2
MD5 428236bee1e89e6bd9cd4c68d70fa2da
BLAKE2b-256 f5d8329d4becf827cdbb3c6af50c10a7b42a5f06a1b731c564e2691cc7c87a91

See more details on using hashes here.

Provenance

The following attestation bundles were made for java_functional_lsp-0.7.8.tar.gz:

Publisher: publish.yml on aviadshiber/java-functional-lsp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file java_functional_lsp-0.7.8-py3-none-any.whl.

File metadata

File hashes

Hashes for java_functional_lsp-0.7.8-py3-none-any.whl
Algorithm Hash digest
SHA256 9af555a4195822c1c813916d620336d05063293d212066aca3fd40a3eb9c5e94
MD5 858f1cd2498d7a198a7644f545c46d68
BLAKE2b-256 d878cff4818748a7bb4095a36e4780d581ebdba075c3da01b4d7937a64f050c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for java_functional_lsp-0.7.8-py3-none-any.whl:

Publisher: publish.yml on aviadshiber/java-functional-lsp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page