java-functional-lsp 0.3.2__tar.gz → 0.4.1__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.3.2 → java_functional_lsp-0.4.1}/.claude-plugin/plugin.json +1 -1
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/PKG-INFO +38 -4
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/README.md +37 -3
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/SKILL.md +22 -3
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/vscode/package.json +1 -1
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/hooks/java_linter_reminder.py +3 -5
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/pyproject.toml +1 -1
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/scripts/generate-formula.py +6 -6
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/analyzers/base.py +24 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/analyzers/exception_checker.py +18 -2
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/analyzers/mutation_checker.py +10 -3
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/cli.py +4 -1
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/server.py +50 -13
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_base.py +35 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_exception_checker.py +42 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_mutation_checker.py +14 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.gitignore +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/LICENSE +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/proxy.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/__init__.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/conftest.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_config.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_proxy.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-functional-lsp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -109,7 +109,18 @@ See [editors/intellij/README.md](editors/intellij/README.md) for detailed instru
|
|
|
109
109
|
|
|
110
110
|
### Claude Code
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
**Step 1: Enable LSP support** (required, one-time):
|
|
113
|
+
|
|
114
|
+
Add to `~/.claude/settings.json`:
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"env": {
|
|
118
|
+
"ENABLE_LSP_TOOL": "1"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Step 2: Install the plugin:**
|
|
113
124
|
|
|
114
125
|
```bash
|
|
115
126
|
claude plugin add https://github.com/aviadshiber/java-functional-lsp.git
|
|
@@ -130,6 +141,23 @@ Or manually add to your Claude Code config:
|
|
|
130
141
|
}
|
|
131
142
|
```
|
|
132
143
|
|
|
144
|
+
**Step 3: Nudge Claude to use diagnostics** (recommended):
|
|
145
|
+
|
|
146
|
+
Add to your project's `CLAUDE.md`:
|
|
147
|
+
```markdown
|
|
148
|
+
After writing or editing Java code, check LSP diagnostics before moving on.
|
|
149
|
+
Fix any violations immediately — do not explain, just apply the fix.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Troubleshooting:**
|
|
153
|
+
|
|
154
|
+
| Issue | Fix |
|
|
155
|
+
|-------|-----|
|
|
156
|
+
| No diagnostics appear | Ensure `ENABLE_LSP_TOOL=1` is set, restart Claude Code |
|
|
157
|
+
| "java-functional-lsp not found" | Run `brew install aviadshiber/tap/java-functional-lsp` |
|
|
158
|
+
| Plugin not active | Run `claude plugin list` to verify, then `/reload-plugins` |
|
|
159
|
+
| Diagnostics slow on first open | Normal — tree-sitter parses on first load, then incremental |
|
|
160
|
+
|
|
133
161
|
### Other Editors
|
|
134
162
|
|
|
135
163
|
Any LSP client that supports stdio transport can use this server. Point it to the `java-functional-lsp` command for `java` files.
|
|
@@ -146,6 +174,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
146
174
|
|
|
147
175
|
```json
|
|
148
176
|
{
|
|
177
|
+
"excludes": ["**/generated/**", "**/vendor/**"],
|
|
149
178
|
"rules": {
|
|
150
179
|
"null-literal-arg": "warning",
|
|
151
180
|
"throw-statement": "info",
|
|
@@ -155,8 +184,13 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
155
184
|
}
|
|
156
185
|
```
|
|
157
186
|
|
|
158
|
-
|
|
159
|
-
|
|
187
|
+
**Options:**
|
|
188
|
+
- `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
|
|
189
|
+
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
190
|
+
|
|
191
|
+
**Spring-aware behavior:**
|
|
192
|
+
- `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
|
|
193
|
+
- `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
|
|
160
194
|
|
|
161
195
|
## How it works
|
|
162
196
|
|
|
@@ -81,7 +81,18 @@ See [editors/intellij/README.md](editors/intellij/README.md) for detailed instru
|
|
|
81
81
|
|
|
82
82
|
### Claude Code
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
**Step 1: Enable LSP support** (required, one-time):
|
|
85
|
+
|
|
86
|
+
Add to `~/.claude/settings.json`:
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"env": {
|
|
90
|
+
"ENABLE_LSP_TOOL": "1"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Step 2: Install the plugin:**
|
|
85
96
|
|
|
86
97
|
```bash
|
|
87
98
|
claude plugin add https://github.com/aviadshiber/java-functional-lsp.git
|
|
@@ -102,6 +113,23 @@ Or manually add to your Claude Code config:
|
|
|
102
113
|
}
|
|
103
114
|
```
|
|
104
115
|
|
|
116
|
+
**Step 3: Nudge Claude to use diagnostics** (recommended):
|
|
117
|
+
|
|
118
|
+
Add to your project's `CLAUDE.md`:
|
|
119
|
+
```markdown
|
|
120
|
+
After writing or editing Java code, check LSP diagnostics before moving on.
|
|
121
|
+
Fix any violations immediately — do not explain, just apply the fix.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Troubleshooting:**
|
|
125
|
+
|
|
126
|
+
| Issue | Fix |
|
|
127
|
+
|-------|-----|
|
|
128
|
+
| No diagnostics appear | Ensure `ENABLE_LSP_TOOL=1` is set, restart Claude Code |
|
|
129
|
+
| "java-functional-lsp not found" | Run `brew install aviadshiber/tap/java-functional-lsp` |
|
|
130
|
+
| Plugin not active | Run `claude plugin list` to verify, then `/reload-plugins` |
|
|
131
|
+
| Diagnostics slow on first open | Normal — tree-sitter parses on first load, then incremental |
|
|
132
|
+
|
|
105
133
|
### Other Editors
|
|
106
134
|
|
|
107
135
|
Any LSP client that supports stdio transport can use this server. Point it to the `java-functional-lsp` command for `java` files.
|
|
@@ -118,6 +146,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
118
146
|
|
|
119
147
|
```json
|
|
120
148
|
{
|
|
149
|
+
"excludes": ["**/generated/**", "**/vendor/**"],
|
|
121
150
|
"rules": {
|
|
122
151
|
"null-literal-arg": "warning",
|
|
123
152
|
"throw-statement": "info",
|
|
@@ -127,8 +156,13 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
|
|
|
127
156
|
}
|
|
128
157
|
```
|
|
129
158
|
|
|
130
|
-
|
|
131
|
-
|
|
159
|
+
**Options:**
|
|
160
|
+
- `excludes` — glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
|
|
161
|
+
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
162
|
+
|
|
163
|
+
**Spring-aware behavior:**
|
|
164
|
+
- `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
|
|
165
|
+
- `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
|
|
132
166
|
|
|
133
167
|
## How it works
|
|
134
168
|
|
|
@@ -45,6 +45,7 @@ Create `.java-functional-lsp.json` in your project root:
|
|
|
45
45
|
|
|
46
46
|
```json
|
|
47
47
|
{
|
|
48
|
+
"excludes": ["**/generated/**", "**/vendor/**"],
|
|
48
49
|
"rules": {
|
|
49
50
|
"imperative-loop": "hint",
|
|
50
51
|
"mutable-variable": "info",
|
|
@@ -53,7 +54,10 @@ Create `.java-functional-lsp.json` in your project root:
|
|
|
53
54
|
}
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
- `excludes` — glob patterns to skip files/directories entirely
|
|
58
|
+
- `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
|
|
59
|
+
- `throw-statement`/`catch-rethrow` auto-suppressed in `@Bean` methods
|
|
60
|
+
- `mutable-dto` suggests `@ConstructorBinding` for `@ConfigurationProperties` classes
|
|
57
61
|
|
|
58
62
|
## On-Demand Linting
|
|
59
63
|
|
|
@@ -70,8 +74,23 @@ To release a new version:
|
|
|
70
74
|
5. CI automatically publishes to PyPI and builds the VS Code extension `.vsix`
|
|
71
75
|
6. Run `python3 scripts/generate-formula.py <version>` and update the Homebrew tap
|
|
72
76
|
|
|
77
|
+
## Enabling LSP in Claude Code
|
|
78
|
+
|
|
79
|
+
LSP support requires `ENABLE_LSP_TOOL=1` in `~/.claude/settings.json`:
|
|
80
|
+
```json
|
|
81
|
+
{ "env": { "ENABLE_LSP_TOOL": "1" } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To nudge Claude to act on diagnostics, add to your project's `CLAUDE.md`:
|
|
85
|
+
```
|
|
86
|
+
After writing or editing Java code, check LSP diagnostics before moving on.
|
|
87
|
+
Fix any violations immediately — do not explain, just apply the fix.
|
|
88
|
+
```
|
|
89
|
+
|
|
73
90
|
## Troubleshooting
|
|
74
91
|
|
|
92
|
+
- **No diagnostics in Claude Code**: Ensure `ENABLE_LSP_TOOL=1` is set, restart Claude Code
|
|
75
93
|
- **"java-functional-lsp not found"**: Run `brew install aviadshiber/tap/java-functional-lsp`
|
|
76
|
-
- **No completions/hover**: Install jdtls: `brew install jdtls`
|
|
77
|
-
- **Too many warnings**: Create `.java-functional-lsp.json`
|
|
94
|
+
- **No completions/hover**: Install jdtls: `brew install jdtls` (requires JDK 21+)
|
|
95
|
+
- **Too many warnings**: Create `.java-functional-lsp.json` with `excludes` or per-rule severity
|
|
96
|
+
- **Plugin not active**: Run `claude plugin list` to verify, then `/reload-plugins`
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "java-functional-lsp",
|
|
3
3
|
"displayName": "Java Functional LSP",
|
|
4
4
|
"description": "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.1",
|
|
6
6
|
"publisher": "aviadshiber",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"engines": {
|
|
@@ -21,11 +21,9 @@ def main() -> None:
|
|
|
21
21
|
"hookSpecificOutput": {
|
|
22
22
|
"hookEventName": "PostToolUse",
|
|
23
23
|
"additionalContext": (
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"mutable → final + functional transforms, loops → .map()/.filter()/.flatMap(), "
|
|
28
|
-
"@Data → @Value, @Autowired → constructor injection, @Component → @Configuration+@Bean."
|
|
24
|
+
"If <new-diagnostics> from java-functional-lsp appear above, "
|
|
25
|
+
"fix each violation immediately. Do not explain or list them — "
|
|
26
|
+
"just apply the fix in your next Edit."
|
|
29
27
|
),
|
|
30
28
|
}
|
|
31
29
|
}
|
|
@@ -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.4.1"
|
|
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" }
|
|
@@ -10,31 +10,31 @@ import sys
|
|
|
10
10
|
import urllib.request
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
14
|
-
"""Get the sdist sha256 for a package from PyPI."""
|
|
13
|
+
def get_pypi_sdist_info(package: str, version: str) -> tuple[str, str]:
|
|
14
|
+
"""Get the sdist URL and sha256 for a package from PyPI."""
|
|
15
15
|
url = f"https://pypi.org/pypi/{package}/{version}/json"
|
|
16
16
|
with urllib.request.urlopen(url) as resp:
|
|
17
17
|
data = json.loads(resp.read())
|
|
18
18
|
|
|
19
19
|
for file_info in data.get("urls", []):
|
|
20
20
|
if file_info["filename"].endswith(".tar.gz"):
|
|
21
|
-
return file_info["digests"]["sha256"]
|
|
21
|
+
return file_info["url"], file_info["digests"]["sha256"]
|
|
22
22
|
|
|
23
23
|
for file_info in data.get("urls", []):
|
|
24
24
|
if file_info["packagetype"] == "sdist":
|
|
25
|
-
return file_info["digests"]["sha256"]
|
|
25
|
+
return file_info["url"], file_info["digests"]["sha256"]
|
|
26
26
|
|
|
27
27
|
raise ValueError(f"No sdist found for {package}=={version}")
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def generate_formula(version: str) -> str:
|
|
31
31
|
"""Generate the Homebrew formula."""
|
|
32
|
-
sha256 =
|
|
32
|
+
sdist_url, sha256 = get_pypi_sdist_info("java-functional-lsp", version)
|
|
33
33
|
|
|
34
34
|
return f'''class JavaFunctionalLsp < Formula
|
|
35
35
|
desc "Java LSP server enforcing functional programming best practices"
|
|
36
36
|
homepage "https://github.com/aviadshiber/java-functional-lsp"
|
|
37
|
-
url "
|
|
37
|
+
url "{sdist_url}"
|
|
38
38
|
sha256 "{sha256}"
|
|
39
39
|
license "MIT"
|
|
40
40
|
|
{java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/src/java_functional_lsp/analyzers/base.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import fnmatch
|
|
5
6
|
from collections.abc import Generator
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from enum import IntEnum
|
|
@@ -147,3 +148,26 @@ def severity_from_config(config: dict[str, Any], rule_id: str, default: Severity
|
|
|
147
148
|
"info": Severity.INFO,
|
|
148
149
|
"hint": Severity.HINT,
|
|
149
150
|
}.get(level, default)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def is_excluded(path_str: str, patterns: list[str]) -> bool:
|
|
154
|
+
"""Return True if path matches any exclude glob pattern.
|
|
155
|
+
|
|
156
|
+
Patterns support ** for multi-segment wildcards (e.g. **/generated/**).
|
|
157
|
+
Uses fnmatch which handles ** correctly across Python 3.10+.
|
|
158
|
+
"""
|
|
159
|
+
normalized = path_str.replace("\\", "/")
|
|
160
|
+
return any(fnmatch.fnmatch(normalized, pattern) for pattern in patterns)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def has_sibling_annotation(modifiers_node: Node, annotation_name: bytes) -> bool:
|
|
164
|
+
"""Check if a modifiers node contains an annotation with the given name.
|
|
165
|
+
|
|
166
|
+
Checks both marker_annotation (@Foo) and annotation (@Foo(...)) forms.
|
|
167
|
+
"""
|
|
168
|
+
for child in modifiers_node.named_children:
|
|
169
|
+
if child.type in ("marker_annotation", "annotation"):
|
|
170
|
+
name_node = child.child_by_field_name("name")
|
|
171
|
+
if name_node is not None and name_node.text == annotation_name:
|
|
172
|
+
return True
|
|
173
|
+
return False
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from .base import Diagnostic, find_nodes, severity_from_config
|
|
7
|
+
from .base import Diagnostic, find_nodes, has_sibling_annotation, severity_from_config
|
|
8
8
|
|
|
9
9
|
_MESSAGES = {
|
|
10
10
|
"throw-statement": ("Avoid throwing exceptions. Use Either.left(error) or Try.of(() -> ...).toEither()."),
|
|
@@ -14,6 +14,19 @@ _MESSAGES = {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _is_in_bean_method(node: Any) -> bool:
|
|
18
|
+
"""Check if node is inside a method annotated with @Bean."""
|
|
19
|
+
parent = node.parent
|
|
20
|
+
while parent:
|
|
21
|
+
if parent.type == "method_declaration":
|
|
22
|
+
modifiers = next((c for c in parent.children if c.type == "modifiers"), None)
|
|
23
|
+
if modifiers and has_sibling_annotation(modifiers, b"Bean"):
|
|
24
|
+
return True
|
|
25
|
+
return False
|
|
26
|
+
parent = parent.parent
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
17
30
|
class ExceptionChecker:
|
|
18
31
|
"""Detects throw statements and catch-rethrow anti-patterns."""
|
|
19
32
|
|
|
@@ -24,6 +37,8 @@ class ExceptionChecker:
|
|
|
24
37
|
severity = severity_from_config(config, "throw-statement")
|
|
25
38
|
if severity is not None:
|
|
26
39
|
for node in find_nodes(tree.root_node, "throw_statement"):
|
|
40
|
+
if _is_in_bean_method(node):
|
|
41
|
+
continue
|
|
27
42
|
diagnostics.append(
|
|
28
43
|
Diagnostic(
|
|
29
44
|
line=node.start_point[0],
|
|
@@ -40,10 +55,11 @@ class ExceptionChecker:
|
|
|
40
55
|
severity = severity_from_config(config, "catch-rethrow")
|
|
41
56
|
if severity is not None:
|
|
42
57
|
for node in find_nodes(tree.root_node, "catch_clause"):
|
|
58
|
+
if _is_in_bean_method(node):
|
|
59
|
+
continue
|
|
43
60
|
body = node.child_by_field_name("body")
|
|
44
61
|
if body is None:
|
|
45
62
|
continue
|
|
46
|
-
# Check if the block has exactly one named statement and it's a throw
|
|
47
63
|
statements = [c for c in body.named_children if c.type not in ("line_comment", "block_comment")]
|
|
48
64
|
if len(statements) == 1 and statements[0].type == "throw_statement":
|
|
49
65
|
diagnostics.append(
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from .base import Diagnostic, find_nodes, find_nodes_multi, has_ancestor, severity_from_config
|
|
7
|
+
from .base import Diagnostic, find_nodes, find_nodes_multi, has_ancestor, has_sibling_annotation, severity_from_config
|
|
8
8
|
|
|
9
9
|
_MESSAGES = {
|
|
10
10
|
"mutable-variable": "Avoid reassigning variables. Use final + functional transforms (map, flatMap, fold).",
|
|
@@ -45,8 +45,15 @@ class MutationChecker:
|
|
|
45
45
|
if ann_text in (b"Data", b"Setter"):
|
|
46
46
|
# Verify it's on a class declaration
|
|
47
47
|
if node.parent and node.parent.type == "modifiers":
|
|
48
|
-
|
|
48
|
+
modifiers = node.parent
|
|
49
|
+
grandparent = modifiers.parent
|
|
49
50
|
if grandparent and grandparent.type == "class_declaration":
|
|
51
|
+
if has_sibling_annotation(modifiers, b"ConfigurationProperties"):
|
|
52
|
+
message = (
|
|
53
|
+
"Use @ConstructorBinding instead of @Data/@Setter for @ConfigurationProperties classes."
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
message = _MESSAGES["mutable-dto"]
|
|
50
57
|
diagnostics.append(
|
|
51
58
|
Diagnostic(
|
|
52
59
|
line=name_node.start_point[0],
|
|
@@ -55,7 +62,7 @@ class MutationChecker:
|
|
|
55
62
|
end_col=name_node.end_point[1],
|
|
56
63
|
severity=severity,
|
|
57
64
|
code="mutable-dto",
|
|
58
|
-
message=
|
|
65
|
+
message=message,
|
|
59
66
|
)
|
|
60
67
|
)
|
|
61
68
|
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from .analyzers.base import Analyzer, Diagnostic, Severity, get_parser
|
|
10
|
+
from .analyzers.base import Analyzer, Diagnostic, Severity, get_parser, is_excluded
|
|
11
11
|
from .analyzers.exception_checker import ExceptionChecker
|
|
12
12
|
from .analyzers.mutation_checker import MutationChecker
|
|
13
13
|
from .analyzers.null_checker import NullChecker
|
|
@@ -107,7 +107,10 @@ def main() -> None:
|
|
|
107
107
|
config = load_config(files[0])
|
|
108
108
|
|
|
109
109
|
total_diags = 0
|
|
110
|
+
excludes: list[str] = config.get("excludes", [])
|
|
110
111
|
for path in files:
|
|
112
|
+
if excludes and is_excluded(path.as_posix(), excludes):
|
|
113
|
+
continue
|
|
111
114
|
diags = check_file(path, config)
|
|
112
115
|
for d in diags:
|
|
113
116
|
print(format_diagnostic(path, d))
|
|
@@ -6,17 +6,19 @@ Proxies to jdtls for full Java language features (completions, hover, go-to-def)
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
import json
|
|
10
11
|
import logging
|
|
12
|
+
import sys
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Any
|
|
13
|
-
from urllib.parse import unquote, urlparse
|
|
14
15
|
|
|
15
16
|
import cattrs
|
|
16
17
|
from lsprotocol import types as lsp
|
|
17
18
|
from pygls.lsp.server import LanguageServer
|
|
19
|
+
from pygls.uris import to_fs_path
|
|
18
20
|
|
|
19
|
-
from .analyzers.base import Analyzer, Severity, get_parser
|
|
21
|
+
from .analyzers.base import Analyzer, Severity, get_parser, is_excluded
|
|
20
22
|
from .analyzers.base import Diagnostic as LintDiagnostic
|
|
21
23
|
from .analyzers.exception_checker import ExceptionChecker
|
|
22
24
|
from .analyzers.mutation_checker import MutationChecker
|
|
@@ -59,11 +61,17 @@ class JavaFunctionalLspServer(LanguageServer):
|
|
|
59
61
|
|
|
60
62
|
server = JavaFunctionalLspServer()
|
|
61
63
|
|
|
64
|
+
# Debounce state for didChange events (only affects human typing in IDEs, not agents)
|
|
65
|
+
_pending: dict[str, asyncio.Task[None]] = {}
|
|
66
|
+
_DEBOUNCE_SECONDS = 0.15
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
|
|
69
|
+
def _handle_exception(exc_type: type[BaseException], exc_value: BaseException, exc_tb: Any) -> None:
|
|
70
|
+
"""Log uncaught exceptions for crash debugging."""
|
|
71
|
+
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
sys.excepthook = _handle_exception
|
|
67
75
|
|
|
68
76
|
|
|
69
77
|
def _load_config(workspace_root: str | None) -> dict[str, Any]:
|
|
@@ -96,6 +104,13 @@ def _to_lsp_diagnostic(diag: LintDiagnostic) -> lsp.Diagnostic:
|
|
|
96
104
|
|
|
97
105
|
def _analyze_document(source_text: str, uri: str = "") -> list[lsp.Diagnostic]:
|
|
98
106
|
"""Run all custom analyzers on the given source text. Uses incremental parsing when possible."""
|
|
107
|
+
# Check excludes before parsing
|
|
108
|
+
if uri:
|
|
109
|
+
excludes: list[str] = server._config.get("excludes", [])
|
|
110
|
+
if excludes:
|
|
111
|
+
path_str = to_fs_path(uri) or uri
|
|
112
|
+
if is_excluded(path_str, excludes):
|
|
113
|
+
return []
|
|
99
114
|
source_bytes = source_text.encode("utf-8")
|
|
100
115
|
old_tree = server._trees.get(uri) if uri else None
|
|
101
116
|
tree = server._parser.parse(source_bytes, old_tree) if old_tree else server._parser.parse(source_bytes)
|
|
@@ -177,7 +192,7 @@ def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|
|
177
192
|
|
|
178
193
|
root = None
|
|
179
194
|
if params.root_uri:
|
|
180
|
-
root =
|
|
195
|
+
root = to_fs_path(params.root_uri)
|
|
181
196
|
elif params.root_path:
|
|
182
197
|
root = params.root_path
|
|
183
198
|
|
|
@@ -216,28 +231,50 @@ async def on_initialized(params: lsp.InitializedParams) -> None:
|
|
|
216
231
|
# --- Document sync (forward to jdtls + run custom analyzers) ---
|
|
217
232
|
|
|
218
233
|
|
|
234
|
+
async def _deferred_validate(uri: str) -> None:
|
|
235
|
+
"""Debounced validation — waits before analyzing to batch rapid edits."""
|
|
236
|
+
await asyncio.sleep(_DEBOUNCE_SECONDS)
|
|
237
|
+
await asyncio.to_thread(_publish_diagnostics, uri)
|
|
238
|
+
|
|
239
|
+
|
|
219
240
|
@server.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
|
|
220
241
|
async def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
|
221
|
-
"""Forward to jdtls and analyze."""
|
|
242
|
+
"""Forward to jdtls and analyze immediately."""
|
|
222
243
|
if server._proxy.is_available:
|
|
223
244
|
await server._proxy.send_notification("textDocument/didOpen", _serialize_params(params))
|
|
224
|
-
_publish_diagnostics
|
|
245
|
+
await asyncio.to_thread(_publish_diagnostics, params.text_document.uri)
|
|
225
246
|
|
|
226
247
|
|
|
227
248
|
@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
|
|
228
249
|
async def on_did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
|
229
|
-
"""Forward to jdtls and re-
|
|
250
|
+
"""Forward to jdtls and schedule debounced re-analysis."""
|
|
251
|
+
uri = params.text_document.uri
|
|
230
252
|
if server._proxy.is_available:
|
|
231
253
|
await server._proxy.send_notification("textDocument/didChange", _serialize_params(params))
|
|
232
|
-
|
|
254
|
+
# Cancel pending validation, schedule new one (150ms debounce for IDE typing)
|
|
255
|
+
if uri in _pending:
|
|
256
|
+
_pending[uri].cancel()
|
|
257
|
+
_pending[uri] = asyncio.create_task(_deferred_validate(uri))
|
|
233
258
|
|
|
234
259
|
|
|
235
260
|
@server.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
|
236
261
|
async def on_did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
|
237
|
-
"""Forward to jdtls and re-analyze."""
|
|
262
|
+
"""Forward to jdtls and re-analyze immediately (no debounce on save)."""
|
|
238
263
|
if server._proxy.is_available:
|
|
239
264
|
await server._proxy.send_notification("textDocument/didSave", _serialize_params(params))
|
|
240
|
-
_publish_diagnostics
|
|
265
|
+
await asyncio.to_thread(_publish_diagnostics, params.text_document.uri)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
|
269
|
+
async def on_did_close(params: lsp.DidCloseTextDocumentParams) -> None:
|
|
270
|
+
"""Clean up cached state and forward to jdtls."""
|
|
271
|
+
uri = params.text_document.uri
|
|
272
|
+
server._trees.pop(uri, None)
|
|
273
|
+
if uri in _pending:
|
|
274
|
+
_pending[uri].cancel()
|
|
275
|
+
del _pending[uri]
|
|
276
|
+
if server._proxy.is_available:
|
|
277
|
+
await server._proxy.send_notification("textDocument/didClose", _serialize_params(params))
|
|
241
278
|
|
|
242
279
|
|
|
243
280
|
# --- Forwarded features (jdtls passthrough) ---
|
|
@@ -7,6 +7,8 @@ from java_functional_lsp.analyzers.base import (
|
|
|
7
7
|
find_nodes_multi,
|
|
8
8
|
get_parser,
|
|
9
9
|
has_ancestor,
|
|
10
|
+
has_sibling_annotation,
|
|
11
|
+
is_excluded,
|
|
10
12
|
)
|
|
11
13
|
|
|
12
14
|
|
|
@@ -120,3 +122,36 @@ class TestCollectNodesByType:
|
|
|
120
122
|
buckets = collect_nodes_by_type(tree.root_node, {"null_literal", "throw_statement"})
|
|
121
123
|
assert len(buckets["null_literal"]) == 0
|
|
122
124
|
assert len(buckets["throw_statement"]) == 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestIsExcluded:
|
|
128
|
+
def test_matches_double_star_pattern(self):
|
|
129
|
+
assert is_excluded("/home/user/project/nlu-trs-client-shaded/Foo.java", ["**/nlu-trs-client-shaded/**"])
|
|
130
|
+
|
|
131
|
+
def test_no_match(self):
|
|
132
|
+
assert not is_excluded("/home/user/project/src/Foo.java", ["**/nlu-trs-client-shaded/**"])
|
|
133
|
+
|
|
134
|
+
def test_empty_patterns(self):
|
|
135
|
+
assert not is_excluded("/any/path/Foo.java", [])
|
|
136
|
+
|
|
137
|
+
def test_multiple_patterns(self):
|
|
138
|
+
assert is_excluded("/project/generated/Model.java", ["**/shaded/**", "**/generated/**"])
|
|
139
|
+
|
|
140
|
+
def test_windows_backslashes_normalized(self):
|
|
141
|
+
assert is_excluded("C:\\project\\generated\\Model.java", ["**/generated/**"])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestHasSiblingAnnotation:
|
|
145
|
+
def test_finds_sibling_annotation(self):
|
|
146
|
+
tree = _parse("@ConfigurationProperties @Setter class Props { String name; }")
|
|
147
|
+
for node in find_nodes(tree.root_node, "marker_annotation"):
|
|
148
|
+
name = node.child_by_field_name("name")
|
|
149
|
+
if name and name.text == b"Setter" and node.parent:
|
|
150
|
+
assert has_sibling_annotation(node.parent, b"ConfigurationProperties")
|
|
151
|
+
|
|
152
|
+
def test_no_sibling(self):
|
|
153
|
+
tree = _parse("@Setter class Props { String name; }")
|
|
154
|
+
for node in find_nodes(tree.root_node, "marker_annotation"):
|
|
155
|
+
name = node.child_by_field_name("name")
|
|
156
|
+
if name and name.text == b"Setter" and node.parent:
|
|
157
|
+
assert not has_sibling_annotation(node.parent, b"ConfigurationProperties")
|
|
@@ -60,3 +60,45 @@ class TestCatchRethrow:
|
|
|
60
60
|
"""
|
|
61
61
|
diags = parse_and_analyze(ExceptionChecker(), source)
|
|
62
62
|
assert not any(d.code == "catch-rethrow" for d in diags)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestBeanSuppression:
|
|
66
|
+
def test_ignores_throw_in_bean_method(self) -> None:
|
|
67
|
+
source = b"""
|
|
68
|
+
class Config {
|
|
69
|
+
@Bean
|
|
70
|
+
DataSource dataSource() {
|
|
71
|
+
if (url == null) {
|
|
72
|
+
throw new IllegalStateException("url required");
|
|
73
|
+
}
|
|
74
|
+
return new DataSource(url);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
"""
|
|
78
|
+
diags = parse_and_analyze(ExceptionChecker(), source)
|
|
79
|
+
assert not any(d.code == "throw-statement" for d in diags)
|
|
80
|
+
|
|
81
|
+
def test_flags_throw_in_regular_method(self) -> None:
|
|
82
|
+
source = b"""
|
|
83
|
+
class Service {
|
|
84
|
+
void process() {
|
|
85
|
+
throw new RuntimeException("error");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
diags = parse_and_analyze(ExceptionChecker(), source)
|
|
90
|
+
assert any(d.code == "throw-statement" for d in diags)
|
|
91
|
+
|
|
92
|
+
def test_ignores_catch_rethrow_in_bean_method(self) -> None:
|
|
93
|
+
source = b"""
|
|
94
|
+
class Config {
|
|
95
|
+
@Bean
|
|
96
|
+
DataSource dataSource() {
|
|
97
|
+
try { return connect(); }
|
|
98
|
+
catch (Exception e) { throw new RuntimeException(e); }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
"""
|
|
102
|
+
diags = parse_and_analyze(ExceptionChecker(), source)
|
|
103
|
+
assert not any(d.code == "catch-rethrow" for d in diags)
|
|
104
|
+
assert not any(d.code == "throw-statement" for d in diags)
|
|
@@ -58,6 +58,20 @@ class TestMutableDto:
|
|
|
58
58
|
diags = parse_and_analyze(MutationChecker(), source)
|
|
59
59
|
assert not any(d.code == "mutable-dto" for d in diags)
|
|
60
60
|
|
|
61
|
+
def test_config_properties_suggests_constructor_binding(self) -> None:
|
|
62
|
+
source = b"@ConfigurationProperties @Setter class Props { String name; }"
|
|
63
|
+
diags = parse_and_analyze(MutationChecker(), source)
|
|
64
|
+
dto_diags = [d for d in diags if d.code == "mutable-dto"]
|
|
65
|
+
assert len(dto_diags) == 1
|
|
66
|
+
assert "@ConstructorBinding" in dto_diags[0].message
|
|
67
|
+
|
|
68
|
+
def test_regular_setter_suggests_value(self) -> None:
|
|
69
|
+
source = b"@Setter class Foo { String name; }"
|
|
70
|
+
diags = parse_and_analyze(MutationChecker(), source)
|
|
71
|
+
dto_diags = [d for d in diags if d.code == "mutable-dto"]
|
|
72
|
+
assert len(dto_diags) == 1
|
|
73
|
+
assert "@Value" in dto_diags[0].message
|
|
74
|
+
|
|
61
75
|
|
|
62
76
|
class TestImperativeOptionUnwrap:
|
|
63
77
|
def test_detects_is_defined_get(self) -> None:
|
|
File without changes
|
{java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/ISSUE_TEMPLATE/bug-report.md
RENAMED
|
File without changes
|
{java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/ISSUE_TEMPLATE/feature-request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/release-drafter.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/.github/workflows/update-homebrew.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.3.2 → java_functional_lsp-0.4.1}/editors/intellij/lsp4ij-template.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|