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.
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/vscode-ext.yml +2 -2
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/CONTRIBUTING.md +5 -2
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/PKG-INFO +107 -18
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/README.md +106 -17
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/SKILL.md +49 -19
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/package-lock.json +14 -14
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/pyproject.toml +1 -1
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/base.py +93 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/exception_checker.py +21 -1
- java_functional_lsp-0.6.0/src/java_functional_lsp/analyzers/functional_checker.py +361 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/mutation_checker.py +42 -1
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/null_checker.py +29 -1
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/spring_checker.py +21 -1
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/cli.py +8 -1
- java_functional_lsp-0.6.0/src/java_functional_lsp/fixes.py +402 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/server.py +85 -3
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_e2e.py +187 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_exception_checker.py +27 -0
- java_functional_lsp-0.6.0/tests/test_fixes.py +252 -0
- java_functional_lsp-0.6.0/tests/test_functional_checker.py +386 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_mutation_checker.py +28 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_null_checker.py +20 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_spring_checker.py +19 -0
- java_functional_lsp-0.6.0/tests/test_suppress.py +246 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/uv.lock +4 -4
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.claude-plugin/plugin.json +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.githooks/pre-commit +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.githooks/pre-push +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/.gitignore +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/LICENSE +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/package.json +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/__main__.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/proxy.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/__init__.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/conftest.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_base.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/tests/test_config.py +0 -0
- {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@
|
|
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@
|
|
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
|
|
49
|
-
5.
|
|
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.
|
|
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
|
[](https://pypi.org/project/java-functional-lsp/)
|
|
34
34
|
[](https://opensource.org/licenses/MIT)
|
|
35
35
|
|
|
36
|
-
A Java Language Server that
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
|
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 (
|
|
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
|
-
|
|
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
|
[](https://pypi.org/project/java-functional-lsp/)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
A Java Language Server that
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
+
"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.
|
|
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.
|
|
1415
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.
|
|
1416
|
-
"integrity": "sha512-
|
|
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.
|
|
2310
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.
|
|
2311
|
-
"integrity": "sha512-
|
|
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.
|
|
3405
|
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.
|
|
3406
|
-
"integrity": "sha512-
|
|
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.
|
|
4413
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.
|
|
4414
|
-
"integrity": "sha512-
|
|
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.
|
|
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" }
|
{java_functional_lsp-0.4.2 → java_functional_lsp-0.6.0}/src/java_functional_lsp/analyzers/base.py
RENAMED
|
@@ -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
|
|