java-functional-lsp 0.4.2__tar.gz → 0.6.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.
Files changed (64) hide show
  1. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/vscode-ext.yml +2 -2
  2. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/CONTRIBUTING.md +5 -2
  3. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/PKG-INFO +107 -18
  4. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/README.md +106 -17
  5. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/SKILL.md +49 -19
  6. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/package-lock.json +14 -14
  7. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/pyproject.toml +1 -1
  8. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/__init__.py +1 -1
  9. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/base.py +93 -0
  10. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/exception_checker.py +21 -1
  11. java_functional_lsp-0.6.0/src/java_functional_lsp/analyzers/functional_checker.py +361 -0
  12. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/mutation_checker.py +42 -1
  13. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/null_checker.py +29 -1
  14. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/spring_checker.py +21 -1
  15. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/cli.py +8 -1
  16. java_functional_lsp-0.6.0/src/java_functional_lsp/fixes.py +402 -0
  17. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/server.py +85 -3
  18. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_e2e.py +187 -0
  19. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_exception_checker.py +27 -0
  20. java_functional_lsp-0.6.0/tests/test_fixes.py +252 -0
  21. java_functional_lsp-0.6.0/tests/test_functional_checker.py +386 -0
  22. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_mutation_checker.py +28 -0
  23. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_null_checker.py +20 -0
  24. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_spring_checker.py +19 -0
  25. java_functional_lsp-0.6.0/tests/test_suppress.py +246 -0
  26. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/uv.lock +4 -4
  27. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.claude-plugin/plugin.json +0 -0
  28. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.githooks/pre-commit +0 -0
  29. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.githooks/pre-push +0 -0
  30. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/CODEOWNERS +0 -0
  31. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  32. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  33. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  34. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/SECURITY.md +0 -0
  35. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/dependabot.yml +0 -0
  36. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/release-drafter.yml +0 -0
  37. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/publish.yml +0 -0
  38. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/release-drafter.yml +0 -0
  39. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/stale.yml +0 -0
  40. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/test.yml +0 -0
  41. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/update-homebrew.yml +0 -0
  42. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.gitignore +0 -0
  43. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/LICENSE +0 -0
  44. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/commands/lint-java.md +0 -0
  45. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/intellij/README.md +0 -0
  46. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/intellij/lsp4ij-template.json +0 -0
  47. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/.vscodeignore +0 -0
  48. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/README.md +0 -0
  49. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/package.json +0 -0
  50. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/src/extension.ts +0 -0
  51. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/tsconfig.json +0 -0
  52. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/hooks/hooks.json +0 -0
  53. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/hooks/java_linter_reminder.py +0 -0
  54. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/scripts/ensure-lsp.sh +0 -0
  55. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/scripts/generate-formula.py +0 -0
  56. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/__main__.py +0 -0
  57. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  58. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/proxy.py +0 -0
  59. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/__init__.py +0 -0
  60. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/conftest.py +0 -0
  61. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_base.py +0 -0
  62. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_cli.py +0 -0
  63. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_config.py +0 -0
  64. {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_proxy.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  - uses: actions/checkout@v6
22
22
 
23
23
  - name: Set up Node.js
24
- uses: actions/setup-node@v4
24
+ uses: actions/setup-node@v6
25
25
  with:
26
26
  node-version: "20"
27
27
 
@@ -35,7 +35,7 @@ jobs:
35
35
  run: npx vsce package
36
36
 
37
37
  - name: Upload VSIX artifact
38
- uses: actions/upload-artifact@v4
38
+ uses: actions/upload-artifact@v7
39
39
  with:
40
40
  name: java-functional-lsp-vsix
41
41
  path: editors/vscode/*.vsix
@@ -45,8 +45,11 @@ uv run pytest
45
45
  1. Choose the appropriate analyzer in `src/java_functional_lsp/analyzers/`
46
46
  2. Add the detection logic using tree-sitter node walking (see `base.py` helpers)
47
47
  3. Add the rule ID and message to the module's `_MESSAGES` dict
48
- 4. Add tests in `tests/test_<analyzer>.py`
49
- 5. Update the rules table in `README.md`
48
+ 4. Add a `DiagnosticData` entry to the module's `_DATA` dict with `fix_type`, `target_library`, and `rationale`
49
+ 5. Pass `data=_DATA["rule-id"]` when creating the `Diagnostic`
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`
52
+ 8. Update the rules table in `README.md`
50
53
 
51
54
  ## Reporting Issues
52
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.4.2
3
+ Version: 0.6.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
@@ -33,24 +33,46 @@ Description-Content-Type: text/markdown
33
33
  [![Python](https://img.shields.io/pypi/pyversions/java-functional-lsp?v=1)](https://pypi.org/project/java-functional-lsp/)
34
34
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
35
 
36
- A Java Language Server that enforces functional programming best practices. Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
36
+ A Java Language Server that provides two things in one:
37
+
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. **15 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
40
+ 3. **Code actions (quick fixes)** — automated refactoring via LSP `textDocument/codeAction`, with machine-readable diagnostic metadata for AI agents
41
+
42
+ Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
37
43
 
38
44
  ## What it checks
39
45
 
40
- | Rule | Detects | Suggests |
41
- |------|---------|----------|
42
- | `null-literal-arg` | `null` passed as method argument | `Option.none()` or default value |
43
- | `null-return` | `return null` | `Option.of()`, `Option.none()`, or `Either` |
44
- | `null-assignment` | `Type x = null` | `Option<Type>` |
45
- | `null-field-assignment` | Field initialized to `null` | `Option<T>` with `Option.none()` |
46
- | `throw-statement` | `throw new XxxException(...)` | `Either.left()` or `Try.of()` |
47
- | `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` |
48
- | `mutable-variable` | Local variable reassignment | Final variables + functional transforms |
49
- | `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()`/`.foldLeft()` |
50
- | `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) |
51
- | `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
52
- | `field-injection` | `@Autowired` on field | Constructor injection |
53
- | `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` |
46
+ ### Java language (via jdtls)
47
+
48
+ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the server proxies all standard Java language features:
49
+
50
+ - Compile errors and warnings
51
+ - Missing imports and unresolved symbols
52
+ - Type mismatches
53
+ - Completions, hover, go-to-definition, find references
54
+
55
+ 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.
56
+
57
+ ### Functional programming rules
58
+
59
+ | Rule | Detects | Suggests | Quick Fix |
60
+ |------|---------|----------|-----------|
61
+ | `null-literal-arg` | `null` passed as method argument | `Option.none()` or default value | — |
62
+ | `null-return` | `return null` | `Option.of()`, `Option.none()`, or `Either` | ✅ |
63
+ | `null-assignment` | `Type x = null` | `Option<Type>` | — |
64
+ | `null-field-assignment` | Field initialized to `null` | `Option<T>` with `Option.none()` | — |
65
+ | `throw-statement` | `throw new XxxException(...)` | `Either.left()` or `Try.of()` | — |
66
+ | `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` | — |
67
+ | `mutable-variable` | Local variable reassignment | Final variables + functional transforms | — |
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()` | — |
71
+ | `field-injection` | `@Autowired` on field | Constructor injection | — |
72
+ | `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
73
+ | `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
74
+ | `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
75
+ | `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
54
76
 
55
77
  ## Install
56
78
 
@@ -64,7 +86,7 @@ pip install java-functional-lsp
64
86
  # From source
65
87
  pip install git+https://github.com/aviadshiber/java-functional-lsp.git
66
88
 
67
- # Optional: install jdtls for full Java language support (completions, hover, go-to-def)
89
+ # Optional: install jdtls for full Java language support (see above)
68
90
  brew install jdtls
69
91
  ```
70
92
 
@@ -212,9 +234,76 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
212
234
  - `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
213
235
  - `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
214
236
 
237
+ **Inline suppression** with `@SuppressWarnings`:
238
+
239
+ ```java
240
+ // Suppress a specific rule on a method
241
+ @SuppressWarnings("java-functional-lsp:null-return")
242
+ public String findUser() { return null; } // no diagnostic
243
+
244
+ // Suppress multiple rules
245
+ @SuppressWarnings({"java-functional-lsp:null-return", "java-functional-lsp:throw-statement"})
246
+ public String findUser() { ... }
247
+
248
+ // Suppress all java-functional-lsp rules
249
+ @SuppressWarnings("java-functional-lsp")
250
+ public String legacyMethod() { ... }
251
+ ```
252
+
253
+ Works on classes, methods, constructors, fields, and local variables. Suppression applies to the annotated scope — a class-level annotation suppresses all methods within it.
254
+
255
+ ## Code actions (quick fixes)
256
+
257
+ 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:
258
+
259
+ | Rule | Code Action | What it does |
260
+ |------|-------------|--------------|
261
+ | `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(...).getOrNull()`, adds import |
263
+ | `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
264
+
265
+ Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
266
+
267
+ ## Agent mode (AI integration)
268
+
269
+ Every diagnostic includes a machine-readable `data` payload designed for AI agents like Claude Code:
270
+
271
+ ```json
272
+ {
273
+ "code": "frozen-mutation",
274
+ "message": "Runtime Exception Risk: Mutating a frozen structure...",
275
+ "data": {
276
+ "fixType": "REPLACE_WITH_VAVR_LIST",
277
+ "targetLibrary": "io.vavr.collection.List",
278
+ "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability."
279
+ }
280
+ }
281
+ ```
282
+
283
+ 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*.
284
+
285
+ **Agent mode configuration** in `.java-functional-lsp.json`:
286
+
287
+ ```json
288
+ {
289
+ "autoImportVavr": true,
290
+ "strictPurity": true
291
+ }
292
+ ```
293
+
294
+ | Key | Default | Effect |
295
+ |-----|---------|--------|
296
+ | `autoImportVavr` | `true` | Quick fixes auto-add Vavr/Option imports |
297
+ | `strictPurity` | `false` | When `true`, `impure-method` uses WARNING severity instead of HINT |
298
+
299
+ > **Note:** The machine-readable `data` payload is always included in diagnostics when available — no configuration needed.
300
+
215
301
  ## How it works
216
302
 
217
- Uses [tree-sitter](https://tree-sitter.github.io/) with the Java grammar for fast, incremental AST parsing. No Java compiler or classpath needed — analysis runs on raw source files.
303
+ The server has two layers:
304
+
305
+ - **Custom rules** — uses [tree-sitter](https://tree-sitter.github.io/) with the Java grammar for sub-millisecond AST analysis (~0.4ms per file). No compiler or classpath needed — runs on raw source files.
306
+ - **Java language features** — proxies [Eclipse jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) for compile errors, completions, hover, go-to-definition, and references. Diagnostics from both layers are merged and published together.
218
307
 
219
308
  The server speaks the Language Server Protocol (LSP) via stdio, making it compatible with any LSP client.
220
309
 
@@ -5,24 +5,46 @@
5
5
  [![Python](https://img.shields.io/pypi/pyversions/java-functional-lsp?v=1)](https://pypi.org/project/java-functional-lsp/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- A Java Language Server that enforces functional programming best practices. Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
8
+ A Java Language Server that provides two things in one:
9
+
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. **15 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
12
+ 3. **Code actions (quick fixes)** — automated refactoring via LSP `textDocument/codeAction`, with machine-readable diagnostic metadata for AI agents
13
+
14
+ Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
9
15
 
10
16
  ## What it checks
11
17
 
12
- | Rule | Detects | Suggests |
13
- |------|---------|----------|
14
- | `null-literal-arg` | `null` passed as method argument | `Option.none()` or default value |
15
- | `null-return` | `return null` | `Option.of()`, `Option.none()`, or `Either` |
16
- | `null-assignment` | `Type x = null` | `Option<Type>` |
17
- | `null-field-assignment` | Field initialized to `null` | `Option<T>` with `Option.none()` |
18
- | `throw-statement` | `throw new XxxException(...)` | `Either.left()` or `Try.of()` |
19
- | `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` |
20
- | `mutable-variable` | Local variable reassignment | Final variables + functional transforms |
21
- | `imperative-loop` | `for`/`while` loops | `.map()`/`.filter()`/`.flatMap()`/`.foldLeft()` |
22
- | `mutable-dto` | `@Data` or `@Setter` on class | `@Value` (immutable) |
23
- | `imperative-option-unwrap` | `if (opt.isDefined()) { opt.get() }` | `map()`/`flatMap()`/`fold()` |
24
- | `field-injection` | `@Autowired` on field | Constructor injection |
25
- | `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` |
18
+ ### Java language (via jdtls)
19
+
20
+ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the server proxies all standard Java language features:
21
+
22
+ - Compile errors and warnings
23
+ - Missing imports and unresolved symbols
24
+ - Type mismatches
25
+ - Completions, hover, go-to-definition, find references
26
+
27
+ 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.
28
+
29
+ ### Functional programming rules
30
+
31
+ | Rule | Detects | Suggests | Quick Fix |
32
+ |------|---------|----------|-----------|
33
+ | `null-literal-arg` | `null` passed as method argument | `Option.none()` or default value | — |
34
+ | `null-return` | `return null` | `Option.of()`, `Option.none()`, or `Either` | ✅ |
35
+ | `null-assignment` | `Type x = null` | `Option<Type>` | — |
36
+ | `null-field-assignment` | Field initialized to `null` | `Option<T>` with `Option.none()` | — |
37
+ | `throw-statement` | `throw new XxxException(...)` | `Either.left()` or `Try.of()` | — |
38
+ | `catch-rethrow` | catch block that wraps + rethrows | `Try.of().toEither()` | — |
39
+ | `mutable-variable` | Local variable reassignment | Final variables + functional transforms | — |
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()` | — |
43
+ | `field-injection` | `@Autowired` on field | Constructor injection | — |
44
+ | `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
45
+ | `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
46
+ | `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
47
+ | `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
26
48
 
27
49
  ## Install
28
50
 
@@ -36,7 +58,7 @@ pip install java-functional-lsp
36
58
  # From source
37
59
  pip install git+https://github.com/aviadshiber/java-functional-lsp.git
38
60
 
39
- # Optional: install jdtls for full Java language support (completions, hover, go-to-def)
61
+ # Optional: install jdtls for full Java language support (see above)
40
62
  brew install jdtls
41
63
  ```
42
64
 
@@ -184,9 +206,76 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
184
206
  - `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
185
207
  - `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
186
208
 
209
+ **Inline suppression** with `@SuppressWarnings`:
210
+
211
+ ```java
212
+ // Suppress a specific rule on a method
213
+ @SuppressWarnings("java-functional-lsp:null-return")
214
+ public String findUser() { return null; } // no diagnostic
215
+
216
+ // Suppress multiple rules
217
+ @SuppressWarnings({"java-functional-lsp:null-return", "java-functional-lsp:throw-statement"})
218
+ public String findUser() { ... }
219
+
220
+ // Suppress all java-functional-lsp rules
221
+ @SuppressWarnings("java-functional-lsp")
222
+ public String legacyMethod() { ... }
223
+ ```
224
+
225
+ Works on classes, methods, constructors, fields, and local variables. Suppression applies to the annotated scope — a class-level annotation suppresses all methods within it.
226
+
227
+ ## Code actions (quick fixes)
228
+
229
+ 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:
230
+
231
+ | Rule | Code Action | What it does |
232
+ |------|-------------|--------------|
233
+ | `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(...).getOrNull()`, adds import |
235
+ | `null-return` | Replace with Option.none() | Rewrites `return null` → `return Option.none()`, adds import |
236
+
237
+ Quick fixes automatically add the required Vavr import if it's not already present. Disable auto-import with `"autoImportVavr": false` in config.
238
+
239
+ ## Agent mode (AI integration)
240
+
241
+ Every diagnostic includes a machine-readable `data` payload designed for AI agents like Claude Code:
242
+
243
+ ```json
244
+ {
245
+ "code": "frozen-mutation",
246
+ "message": "Runtime Exception Risk: Mutating a frozen structure...",
247
+ "data": {
248
+ "fixType": "REPLACE_WITH_VAVR_LIST",
249
+ "targetLibrary": "io.vavr.collection.List",
250
+ "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException. Use Vavr for safe, persistent immutability."
251
+ }
252
+ }
253
+ ```
254
+
255
+ 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*.
256
+
257
+ **Agent mode configuration** in `.java-functional-lsp.json`:
258
+
259
+ ```json
260
+ {
261
+ "autoImportVavr": true,
262
+ "strictPurity": true
263
+ }
264
+ ```
265
+
266
+ | Key | Default | Effect |
267
+ |-----|---------|--------|
268
+ | `autoImportVavr` | `true` | Quick fixes auto-add Vavr/Option imports |
269
+ | `strictPurity` | `false` | When `true`, `impure-method` uses WARNING severity instead of HINT |
270
+
271
+ > **Note:** The machine-readable `data` payload is always included in diagnostics when available — no configuration needed.
272
+
187
273
  ## How it works
188
274
 
189
- Uses [tree-sitter](https://tree-sitter.github.io/) with the Java grammar for fast, incremental AST parsing. No Java compiler or classpath needed — analysis runs on raw source files.
275
+ The server has two layers:
276
+
277
+ - **Custom rules** — uses [tree-sitter](https://tree-sitter.github.io/) with the Java grammar for sub-millisecond AST analysis (~0.4ms per file). No compiler or classpath needed — runs on raw source files.
278
+ - **Java language features** — proxies [Eclipse jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) for compile errors, completions, hover, go-to-definition, and references. Diagnostics from both layers are merged and published together.
190
279
 
191
280
  The server speaks the Language Server Protocol (LSP) via stdio, making it compatible with any LSP client.
192
281
 
@@ -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 12 functional programming rules enforcement. Auto-invoke when setting up Java language support or discussing Java linting configuration.
3
+ 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.
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 12 functional programming rules. Gives you **full Java language support** (completions, hover, go-to-def, compile errors) **plus** custom diagnostics — all before compilation.
10
+ 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.
11
11
 
12
12
  ## Prerequisites
13
13
 
@@ -22,22 +22,47 @@ 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 (12 checks)
26
-
27
- | Rule | Detects | Suggests |
28
- |------|---------|----------|
29
- | `null-literal-arg` | `null` passed as argument | `Option.none()` or default |
30
- | `null-return` | `return null` | `Option.of()`, `Option.none()`, or `Either` |
31
- | `null-assignment` | `Type x = null` | `Option<Type>` |
32
- | `null-field-assignment` | Field initialized to `null` | `Option<T>` with `Option.none()` |
33
- | `throw-statement` | `throw new XxxException(...)` | `Either.left()` or `Try.of()` |
34
- | `catch-rethrow` | catch wraps + rethrows | `Try.of().toEither()` |
35
- | `mutable-variable` | Variable reassignment | Final + functional transforms |
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()` |
39
- | `field-injection` | `@Autowired` on field | Constructor injection |
40
- | `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` |
25
+ ## Rules (15 checks)
26
+
27
+ | Rule | Detects | Suggests | Quick Fix |
28
+ |------|---------|----------|-----------|
29
+ | `null-literal-arg` | `null` passed as argument | `Option.none()` or default | — |
30
+ | `null-return` | `return null` | `Option.of()`, `Option.none()`, or `Either` | ✅ |
31
+ | `null-assignment` | `Type x = null` | `Option<Type>` | — |
32
+ | `null-field-assignment` | Field initialized to `null` | `Option<T>` with `Option.none()` | — |
33
+ | `throw-statement` | `throw new XxxException(...)` | `Either.left()` or `Try.of()` | — |
34
+ | `catch-rethrow` | catch wraps + rethrows | `Try.of().toEither()` | — |
35
+ | `mutable-variable` | Variable reassignment | Final + functional transforms | — |
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()` | — |
39
+ | `field-injection` | `@Autowired` on field | Constructor injection | — |
40
+ | `component-annotation` | `@Component`/`@Service`/`@Repository` | `@Configuration` + `@Bean` | — |
41
+ | `frozen-mutation` | Mutation on `List.of()`/`Collections.unmodifiable*` | `io.vavr.collection.List` | ✅ |
42
+ | `null-check-to-monadic` | `if (x != null) { return x.foo(); }` | `Option.of(x).map(...)` | ✅ |
43
+ | `impure-method` | Method mixing pure logic with side-effects | Extract pure logic; wrap IO in `Try` | — |
44
+
45
+ ## Code Actions (Quick Fixes)
46
+
47
+ Rules marked ✅ provide automated `textDocument/codeAction` fixes:
48
+
49
+ - **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-return** → "Replace with Option.none()" — replaces `null` with `Option.none()`, adds import
52
+
53
+ ## Agent-Ready Diagnostics
54
+
55
+ Every diagnostic includes a machine-readable `data` payload:
56
+
57
+ ```json
58
+ {
59
+ "fixType": "REPLACE_WITH_VAVR_LIST",
60
+ "targetLibrary": "io.vavr.collection.List",
61
+ "rationale": "Runtime mutation of List.of() causes UnsupportedOperationException."
62
+ }
63
+ ```
64
+
65
+ This lets AI agents apply fixes with confidence — `fixType` says what to do, `targetLibrary` says which dependency, `rationale` says why.
41
66
 
42
67
  ## Configuration
43
68
 
@@ -50,14 +75,19 @@ Create `.java-functional-lsp.json` in your project root:
50
75
  "imperative-loop": "hint",
51
76
  "mutable-variable": "info",
52
77
  "throw-statement": "off"
53
- }
78
+ },
79
+ "autoImportVavr": true,
80
+ "strictPurity": false
54
81
  }
55
82
  ```
56
83
 
57
84
  - `excludes` — glob patterns to skip files/directories entirely
58
85
  - `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
86
+ - `autoImportVavr` — quick fixes auto-add Vavr imports (default: `true`)
87
+ - `strictPurity` — `impure-method` uses WARNING instead of HINT (default: `false`)
59
88
  - `throw-statement`/`catch-rethrow` auto-suppressed in `@Bean` methods
60
89
  - `mutable-dto` suggests `@ConstructorBinding` for `@ConfigurationProperties` classes
90
+ - Inline suppression: `@SuppressWarnings("java-functional-lsp:rule-id")` on any declaration
61
91
 
62
92
  ## Automatic Enforcement
63
93
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "java-functional-lsp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "java-functional-lsp",
9
- "version": "0.3.0",
9
+ "version": "0.4.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "vscode-languageclient": "^9.0.0"
@@ -1411,9 +1411,9 @@
1411
1411
  "license": "BSD-2-Clause"
1412
1412
  },
1413
1413
  "node_modules/brace-expansion": {
1414
- "version": "1.1.12",
1415
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
1416
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
1414
+ "version": "1.1.13",
1415
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
1416
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
1417
1417
  "dev": true,
1418
1418
  "license": "MIT",
1419
1419
  "dependencies": {
@@ -2306,9 +2306,9 @@
2306
2306
  }
2307
2307
  },
2308
2308
  "node_modules/glob/node_modules/brace-expansion": {
2309
- "version": "5.0.4",
2310
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
2311
- "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
2309
+ "version": "5.0.5",
2310
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
2311
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
2312
2312
  "dev": true,
2313
2313
  "license": "MIT",
2314
2314
  "dependencies": {
@@ -3401,9 +3401,9 @@
3401
3401
  "license": "ISC"
3402
3402
  },
3403
3403
  "node_modules/picomatch": {
3404
- "version": "2.3.1",
3405
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
3406
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
3404
+ "version": "2.3.2",
3405
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
3406
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
3407
3407
  "dev": true,
3408
3408
  "license": "MIT",
3409
3409
  "engines": {
@@ -4409,9 +4409,9 @@
4409
4409
  }
4410
4410
  },
4411
4411
  "node_modules/vscode-languageclient/node_modules/brace-expansion": {
4412
- "version": "2.0.2",
4413
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
4414
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
4412
+ "version": "2.0.3",
4413
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
4414
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
4415
4415
  "license": "MIT",
4416
4416
  "dependencies": {
4417
4417
  "balanced-match": "^1.0.0"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.4.2"
7
+ version = "0.6.0"
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" }
@@ -1,3 +1,3 @@
1
1
  """java-functional-lsp: A Java LSP server enforcing functional programming best practices."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.6.0"
@@ -19,6 +19,15 @@ class Severity(IntEnum):
19
19
  HINT = 4
20
20
 
21
21
 
22
+ @dataclass(frozen=True)
23
+ class DiagnosticData:
24
+ """Machine-readable metadata for AI agents and automated refactoring."""
25
+
26
+ fix_type: str # e.g. "REPLACE_WITH_VAVR_LIST", "WRAP_IN_OPTION"
27
+ target_library: str # e.g. "io.vavr.collection.List"
28
+ rationale: str # human+machine readable explanation
29
+
30
+
22
31
  @dataclass(frozen=True)
23
32
  class Diagnostic:
24
33
  line: int # 0-based
@@ -29,6 +38,7 @@ class Diagnostic:
29
38
  code: str # rule ID
30
39
  message: str
31
40
  source: str = "java-functional-lsp"
41
+ data: DiagnosticData | None = None
32
42
 
33
43
 
34
44
  class Analyzer(Protocol):
@@ -160,6 +170,89 @@ def is_excluded(path_str: str, patterns: list[str]) -> bool:
160
170
  return any(fnmatch.fnmatch(normalized, pattern) for pattern in patterns)
161
171
 
162
172
 
173
+ _SUPPRESS_PREFIX = "java-functional-lsp"
174
+ _DECLARATION_TYPES = frozenset(
175
+ {
176
+ "method_declaration",
177
+ "class_declaration",
178
+ "interface_declaration",
179
+ "enum_declaration",
180
+ "record_declaration",
181
+ "field_declaration",
182
+ "local_variable_declaration",
183
+ "constructor_declaration",
184
+ }
185
+ )
186
+
187
+
188
+ def is_suppressed(root: Node, line: int, col: int, rule_id: str) -> bool:
189
+ """Check if a diagnostic at (line, col) is suppressed by @SuppressWarnings."""
190
+ node = root.descendant_for_point_range((line, col), (line, col))
191
+ if node is None:
192
+ return False
193
+ current: Node | None = node
194
+ while current is not None:
195
+ if current.type in _DECLARATION_TYPES:
196
+ modifiers = next((c for c in current.children if c.type == "modifiers"), None)
197
+ if modifiers and _modifiers_suppress(modifiers, rule_id):
198
+ return True
199
+ current = current.parent
200
+ return False
201
+
202
+
203
+ def _modifiers_suppress(modifiers: Node, rule_id: str) -> bool:
204
+ """Check if modifiers contain @SuppressWarnings suppressing the given rule."""
205
+ for child in modifiers.named_children:
206
+ if child.type == "annotation":
207
+ name_node = child.child_by_field_name("name")
208
+ if name_node and name_node.text == b"SuppressWarnings":
209
+ args = child.child_by_field_name("arguments")
210
+ if args and _annotation_args_suppress(args, rule_id):
211
+ return True
212
+ return False
213
+
214
+
215
+ def _annotation_args_suppress(args: Node, rule_id: str) -> bool:
216
+ """Parse @SuppressWarnings value(s) and check for rule match."""
217
+ for string_node in find_nodes(args, "string_literal"):
218
+ if string_node.text is None or len(string_node.text) < len('""'):
219
+ continue
220
+ value = string_node.text[1:-1].decode("utf-8")
221
+ if value == _SUPPRESS_PREFIX:
222
+ return True
223
+ if value == f"{_SUPPRESS_PREFIX}:{rule_id}":
224
+ return True
225
+ return False
226
+
227
+
228
+ def extract_null_check_var(condition: Node) -> bytes | None:
229
+ """Extract the variable from a `x != null` or `null != x` binary condition.
230
+
231
+ Handles parenthesized expressions. Returns the variable name as bytes, or None.
232
+ """
233
+ node = condition
234
+ if node.type == "parenthesized_expression" and node.named_child_count == 1:
235
+ node = node.named_children[0]
236
+
237
+ if node.type != "binary_expression":
238
+ return None
239
+
240
+ # Must have != operator (tree-sitter stores operators as unnamed children)
241
+ if not any(c.type == "!=" for c in node.children):
242
+ return None
243
+
244
+ left = node.child_by_field_name("left")
245
+ right = node.child_by_field_name("right")
246
+ if left is None or right is None:
247
+ return None
248
+
249
+ var_node = left if right.type == "null_literal" else (right if left.type == "null_literal" else None)
250
+ if var_node is not None and var_node.type == "identifier":
251
+ val: bytes | None = var_node.text
252
+ return val
253
+ return None
254
+
255
+
163
256
  def has_sibling_annotation(modifiers_node: Node, annotation_name: bytes) -> bool:
164
257
  """Check if a modifiers node contains an annotation with the given name.
165
258