java-functional-lsp 0.9.14__tar.gz → 0.9.16__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 (70) hide show
  1. java_functional_lsp-0.9.16/.claude-plugin/plugin.json +16 -0
  2. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/PKG-INFO +21 -7
  3. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/README.md +20 -6
  4. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/SKILL.md +3 -6
  5. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/intellij/README.md +1 -1
  6. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/pyproject.toml +1 -1
  7. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/__init__.py +1 -1
  8. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/cli.py +34 -25
  9. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_cli.py +27 -1
  10. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_server.py +37 -12
  11. java_functional_lsp-0.9.14/.claude-plugin/plugin.json +0 -5
  12. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.githooks/pre-commit +0 -0
  13. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.githooks/pre-push +0 -0
  14. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/CODEOWNERS +0 -0
  15. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  16. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  17. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/SECURITY.md +0 -0
  19. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/dependabot.yml +0 -0
  20. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/release-drafter.yml +0 -0
  21. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/workflows/publish.yml +0 -0
  22. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/workflows/release-drafter.yml +0 -0
  23. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/workflows/stale.yml +0 -0
  24. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/workflows/test.yml +0 -0
  25. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/workflows/update-homebrew.yml +0 -0
  26. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.github/workflows/vscode-ext.yml +0 -0
  27. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/.gitignore +0 -0
  28. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/CONTRIBUTING.md +0 -0
  29. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/LICENSE +0 -0
  30. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/commands/lint-java.md +0 -0
  31. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/intellij/lsp4ij-template.json +0 -0
  32. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/vscode/.vscodeignore +0 -0
  33. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/vscode/README.md +0 -0
  34. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/vscode/package-lock.json +0 -0
  35. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/vscode/package.json +0 -0
  36. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/vscode/src/extension.ts +0 -0
  37. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/editors/vscode/tsconfig.json +0 -0
  38. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/hooks/hooks.json +0 -0
  39. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/hooks/java_linter_reminder.py +0 -0
  40. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/scripts/ensure-lsp.sh +0 -0
  41. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/scripts/generate-formula.py +0 -0
  42. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/__main__.py +0 -0
  43. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  44. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/analyzers/base.py +0 -0
  45. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  46. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  47. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  48. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  49. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  50. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/fixes.py +0 -0
  51. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/merkle.py +0 -0
  52. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/proxy.py +0 -0
  53. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/src/java_functional_lsp/server.py +0 -0
  54. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/__init__.py +0 -0
  55. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/conftest.py +0 -0
  56. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_base.py +0 -0
  57. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_config.py +0 -0
  58. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_e2e.py +0 -0
  59. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_e2e_jdtls.py +0 -0
  60. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_exception_checker.py +0 -0
  61. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_fixes.py +0 -0
  62. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_functional_checker.py +0 -0
  63. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_merkle.py +0 -0
  64. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_merkle_proxy.py +0 -0
  65. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_mutation_checker.py +0 -0
  66. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_null_checker.py +0 -0
  67. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_proxy.py +0 -0
  68. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_spring_checker.py +0 -0
  69. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/tests/test_suppress.py +0 -0
  70. {java_functional_lsp-0.9.14 → java_functional_lsp-0.9.16}/uv.lock +0 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "java-functional-lsp",
3
+ "description": "Java LSP with functional programming rules enforcement — null safety, immutability, no exceptions, Spring best practices. Wraps jdtls for full Java language support.",
4
+ "version": "0.4.2",
5
+ "lspServers": {
6
+ "java-functional": {
7
+ "command": "java-functional-lsp",
8
+ "extensionToLanguage": {
9
+ ".java": "java"
10
+ },
11
+ "startupTimeout": 120000,
12
+ "restartOnCrash": true,
13
+ "maxRestarts": 5
14
+ }
15
+ }
16
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.9.14
3
+ Version: 0.9.16
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
@@ -136,11 +136,17 @@ See [editors/intellij/README.md](editors/intellij/README.md) for detailed instru
136
136
 
137
137
  **Step 1: Enable LSP support** (required, one-time):
138
138
 
139
- Add to `~/.claude/settings.json`:
139
+ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatically — only needed for manual setup):
140
140
  ```json
141
141
  {
142
- "env": {
143
- "ENABLE_LSP_TOOL": "1"
142
+ "lspServers": {
143
+ "java-functional": {
144
+ "command": "java-functional-lsp",
145
+ "extensionToLanguage": { ".java": "java" },
146
+ "startupTimeout": 120000,
147
+ "restartOnCrash": true,
148
+ "maxRestarts": 5
149
+ }
144
150
  }
145
151
  }
146
152
  ```
@@ -160,19 +166,27 @@ Or manually add to your Claude Code config:
160
166
  "lspServers": {
161
167
  "java-functional": {
162
168
  "command": "java-functional-lsp",
163
- "extensionToLanguage": { ".java": "java" }
169
+ "extensionToLanguage": { ".java": "java" },
170
+ "startupTimeout": 120000,
171
+ "restartOnCrash": true,
172
+ "maxRestarts": 5
164
173
  }
165
174
  }
166
175
  }
167
176
  ```
168
177
 
178
+ (`startupTimeout: 120000` accommodates jdtls cold-start; `restartOnCrash` keeps the server alive across session.)
179
+
169
180
  **Alternative: project-level `.lsp.json`** — instead of installing the plugin or editing global config, add a `.lsp.json` file at your project root:
170
181
 
171
182
  ```json
172
183
  {
173
184
  "java-functional": {
174
185
  "command": "java-functional-lsp",
175
- "extensionToLanguage": { ".java": "java" }
186
+ "extensionToLanguage": { ".java": "java" },
187
+ "startupTimeout": 120000,
188
+ "restartOnCrash": true,
189
+ "maxRestarts": 5
176
190
  }
177
191
  }
178
192
  ```
@@ -198,7 +212,7 @@ moving on. Fix any type errors or missing imports immediately.
198
212
 
199
213
  | Issue | Fix |
200
214
  |-------|-----|
201
- | No diagnostics appear | Ensure `ENABLE_LSP_TOOL=1` is set, restart Claude Code |
215
+ | No diagnostics appear | Ensure `lspServers` is configured (plugin or settings.json), restart |
202
216
  | "java-functional-lsp not found" | Run `brew install aviadshiber/tap/java-functional-lsp` |
203
217
  | Plugin not active | Run `claude plugin list` to verify, then `/reload-plugins` |
204
218
  | Diagnostics slow on first open | Normal — tree-sitter parses on first load, then incremental |
@@ -108,11 +108,17 @@ See [editors/intellij/README.md](editors/intellij/README.md) for detailed instru
108
108
 
109
109
  **Step 1: Enable LSP support** (required, one-time):
110
110
 
111
- Add to `~/.claude/settings.json`:
111
+ Add `lspServers` to `~/.claude/settings.json` (the plugin handles this automatically — only needed for manual setup):
112
112
  ```json
113
113
  {
114
- "env": {
115
- "ENABLE_LSP_TOOL": "1"
114
+ "lspServers": {
115
+ "java-functional": {
116
+ "command": "java-functional-lsp",
117
+ "extensionToLanguage": { ".java": "java" },
118
+ "startupTimeout": 120000,
119
+ "restartOnCrash": true,
120
+ "maxRestarts": 5
121
+ }
116
122
  }
117
123
  }
118
124
  ```
@@ -132,19 +138,27 @@ Or manually add to your Claude Code config:
132
138
  "lspServers": {
133
139
  "java-functional": {
134
140
  "command": "java-functional-lsp",
135
- "extensionToLanguage": { ".java": "java" }
141
+ "extensionToLanguage": { ".java": "java" },
142
+ "startupTimeout": 120000,
143
+ "restartOnCrash": true,
144
+ "maxRestarts": 5
136
145
  }
137
146
  }
138
147
  }
139
148
  ```
140
149
 
150
+ (`startupTimeout: 120000` accommodates jdtls cold-start; `restartOnCrash` keeps the server alive across session.)
151
+
141
152
  **Alternative: project-level `.lsp.json`** — instead of installing the plugin or editing global config, add a `.lsp.json` file at your project root:
142
153
 
143
154
  ```json
144
155
  {
145
156
  "java-functional": {
146
157
  "command": "java-functional-lsp",
147
- "extensionToLanguage": { ".java": "java" }
158
+ "extensionToLanguage": { ".java": "java" },
159
+ "startupTimeout": 120000,
160
+ "restartOnCrash": true,
161
+ "maxRestarts": 5
148
162
  }
149
163
  }
150
164
  ```
@@ -170,7 +184,7 @@ moving on. Fix any type errors or missing imports immediately.
170
184
 
171
185
  | Issue | Fix |
172
186
  |-------|-----|
173
- | No diagnostics appear | Ensure `ENABLE_LSP_TOOL=1` is set, restart Claude Code |
187
+ | No diagnostics appear | Ensure `lspServers` is configured (plugin or settings.json), restart |
174
188
  | "java-functional-lsp not found" | Run `brew install aviadshiber/tap/java-functional-lsp` |
175
189
  | Plugin not active | Run `claude plugin list` to verify, then `/reload-plugins` |
176
190
  | Diagnostics slow on first open | Normal — tree-sitter parses on first load, then incremental |
@@ -112,14 +112,11 @@ To release a new version:
112
112
 
113
113
  ## Enabling LSP in Claude Code
114
114
 
115
- LSP support requires `ENABLE_LSP_TOOL=1` in `~/.claude/settings.json`:
116
- ```json
117
- { "env": { "ENABLE_LSP_TOOL": "1" } }
118
- ```
115
+ Declare `lspServers` in `~/.claude/settings.json` or in `plugin.json` — Claude Code enables the LSP tool automatically when `lspServers` is configured. `ENABLE_LSP_TOOL=1` is no longer needed.
119
116
 
120
117
  For containers or CI, add a `.lsp.json` at the project root instead of installing the plugin:
121
118
  ```json
122
- { "java-functional": { "command": "java-functional-lsp", "extensionToLanguage": { ".java": "java" } } }
119
+ { "java-functional": { "command": "java-functional-lsp", "extensionToLanguage": { ".java": "java" }, "startupTimeout": 120000, "restartOnCrash": true, "maxRestarts": 5 } }
123
120
  ```
124
121
 
125
122
  To nudge Claude to act on diagnostics, add to your project's `CLAUDE.md`:
@@ -130,7 +127,7 @@ Fix any violations immediately — do not explain, just apply the fix.
130
127
 
131
128
  ## Troubleshooting
132
129
 
133
- - **No diagnostics in Claude Code**: Ensure `ENABLE_LSP_TOOL=1` is set, restart Claude Code
130
+ - **No diagnostics in Claude Code**: Ensure `lspServers` is configured (in `plugin.json` or `settings.json`), restart Claude Code
134
131
  - **"java-functional-lsp not found"**: Run `brew install aviadshiber/tap/java-functional-lsp`
135
132
  - **No completions/hover**: Install jdtls: `brew install jdtls` (requires JDK 21+)
136
133
  - **Too many warnings**: Create `.java-functional-lsp.json` with `excludes` or per-rule severity
@@ -84,7 +84,7 @@ The server automatically detects JetBrains IDEs and disables the jdtls proxy —
84
84
  ### No diagnostics appear
85
85
  - Ensure the file mapping is set to Language: `Java`, Language ID: `java`
86
86
  - Check that `.java-functional-lsp.json` doesn't have rules set to `"off"`
87
- - Try restarting the language server: **Tools** → **Language Servers** → **Restart**
87
+ - Try restarting the language server: **Language Servers** (search it in intellij Actions [Double Shift on keyboard]) → click on java-functional-lsp → **Restart**
88
88
  - If diagnostics worked before but stopped, the jdtls proxy may be interfering. Set `JAVA_FUNCTIONAL_LSP_JDTLS=off` in the server command environment to force-disable jdtls. The server auto-detects JetBrains IDEs and disables jdtls by default (since IntelliJ provides native Java support), but this can be overridden with `JAVA_FUNCTIONAL_LSP_JDTLS=on`.
89
89
 
90
90
  ### PATH not found
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.9.14"
7
+ version = "0.9.16"
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.9.14"
3
+ __version__ = "0.9.16"
@@ -66,14 +66,41 @@ def format_diagnostic(path: Path, d: Diagnostic) -> str:
66
66
  return f"{path}:{d.line + 1}:{d.col}: [{sym}] {d.code}: {d.message}"
67
67
 
68
68
 
69
+ _USAGE = """\
70
+ Usage: java-functional-lsp check <file.java> [file2.java ...]
71
+ java-functional-lsp check --dir <directory>
72
+ java-functional-lsp (start LSP server on stdio)"""
73
+
74
+
75
+ def _collect_files(args: list[str]) -> list[Path]:
76
+ """Resolve CLI args to a list of .java paths."""
77
+ files: list[Path] = []
78
+ if args and args[0] == "--dir":
79
+ if len(args) < 2:
80
+ print("Error: --dir requires a directory path", file=sys.stderr)
81
+ sys.exit(1)
82
+ directory = Path(args[1])
83
+ if not directory.is_dir():
84
+ print(f"Error: {directory} is not a directory", file=sys.stderr)
85
+ sys.exit(1)
86
+ return sorted(directory.rglob("*.java"))
87
+ for arg in args:
88
+ p = Path(arg)
89
+ if p.is_file():
90
+ files.append(p)
91
+ elif p.is_dir():
92
+ files.extend(sorted(p.rglob("*.java")))
93
+ else:
94
+ print(f"Warning: {arg} not found, skipping", file=sys.stderr)
95
+ return files
96
+
97
+
69
98
  def main() -> None:
70
99
  """CLI entry point: java-functional-lsp check <files...>"""
71
100
  args = sys.argv[1:]
72
101
 
73
102
  if args and args[0] in ("-h", "--help"):
74
- print("Usage: java-functional-lsp check <file.java> [file2.java ...]")
75
- print(" java-functional-lsp check --dir <directory>")
76
- print(" java-functional-lsp (start LSP server on stdio)")
103
+ print(_USAGE)
77
104
  sys.exit(0)
78
105
 
79
106
  if not args or args[0] != "check":
@@ -85,34 +112,16 @@ def main() -> None:
85
112
 
86
113
  args = args[1:] # skip "check"
87
114
 
88
- # Collect files
89
- files: list[Path] = []
90
- if args and args[0] == "--dir":
91
- if len(args) < 2:
92
- print("Error: --dir requires a directory path", file=sys.stderr)
93
- sys.exit(1)
94
- directory = Path(args[1])
95
- if not directory.is_dir():
96
- print(f"Error: {directory} is not a directory", file=sys.stderr)
97
- sys.exit(1)
98
- files = sorted(directory.rglob("*.java"))
99
- else:
100
- for arg in args:
101
- p = Path(arg)
102
- if p.is_file():
103
- files.append(p)
104
- elif p.is_dir():
105
- files.extend(sorted(p.rglob("*.java")))
106
- else:
107
- print(f"Warning: {arg} not found, skipping", file=sys.stderr)
115
+ if args and args[0] in ("-h", "--help"):
116
+ print(_USAGE)
117
+ sys.exit(0)
108
118
 
119
+ files = _collect_files(args)
109
120
  if not files:
110
121
  print("No .java files found", file=sys.stderr)
111
122
  sys.exit(1)
112
123
 
113
- # Load config from first file's directory
114
124
  config = load_config(files[0])
115
-
116
125
  total_diags = 0
117
126
  excludes: list[str] = config.get("excludes", [])
118
127
  for path in files:
@@ -4,8 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
6
  from typing import Any
7
+ from unittest.mock import patch
7
8
 
8
- from java_functional_lsp.cli import check_file, format_diagnostic, load_config
9
+ import pytest
10
+
11
+ from java_functional_lsp.cli import check_file, format_diagnostic, load_config, main
9
12
 
10
13
 
11
14
  class TestCheckFile:
@@ -53,3 +56,26 @@ class TestLoadConfig:
53
56
  def test_missing_config(self, tmp_path: Path) -> None:
54
57
  config = load_config(tmp_path / "Test.java")
55
58
  assert config == {}
59
+
60
+
61
+ class TestHelp:
62
+ def test_top_level_help(self, capsys: pytest.CaptureFixture[str]) -> None:
63
+ with patch("sys.argv", ["java-functional-lsp", "--help"]):
64
+ with pytest.raises(SystemExit) as exc:
65
+ main()
66
+ assert exc.value.code == 0
67
+ assert "check" in capsys.readouterr().out
68
+
69
+ def test_check_subcommand_help(self, capsys: pytest.CaptureFixture[str]) -> None:
70
+ with patch("sys.argv", ["java-functional-lsp", "check", "--help"]):
71
+ with pytest.raises(SystemExit) as exc:
72
+ main()
73
+ assert exc.value.code == 0
74
+ assert "check" in capsys.readouterr().out
75
+
76
+ def test_check_short_help(self, capsys: pytest.CaptureFixture[str]) -> None:
77
+ with patch("sys.argv", ["java-functional-lsp", "check", "-h"]):
78
+ with pytest.raises(SystemExit) as exc:
79
+ main()
80
+ assert exc.value.code == 0
81
+ assert "check" in capsys.readouterr().out
@@ -71,10 +71,15 @@ async def lsp_client(tmp_path: Any) -> LanguageClient: # type: ignore[misc]
71
71
 
72
72
  # Collect published diagnostics so tests can assert on them.
73
73
  client._published: dict[str, list[lsp.Diagnostic]] = {} # type: ignore[attr-defined]
74
+ # Per-URI events signalled by on_publish — used by _open_and_wait_for_diagnostics.
75
+ client._diag_events: dict[str, asyncio.Event] = {} # type: ignore[attr-defined]
74
76
 
75
77
  @client.feature(lsp.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS)
76
78
  def on_publish(params: lsp.PublishDiagnosticsParams) -> None:
77
79
  client._published[params.uri] = list(params.diagnostics) # type: ignore[attr-defined]
80
+ evt = client._diag_events.get(params.uri) # type: ignore[attr-defined]
81
+ if evt is not None:
82
+ evt.set()
78
83
 
79
84
  await client.start_io(sys.executable, "-m", "java_functional_lsp")
80
85
 
@@ -104,12 +109,17 @@ async def lsp_client(tmp_path: Any) -> LanguageClient: # type: ignore[misc]
104
109
  try:
105
110
  yield client
106
111
  finally:
112
+ # Teardown with timeouts: the server subprocess may be mid-jdtls-startup
113
+ # on a slow CI runner, causing shutdown_async or stop to hang indefinitely.
107
114
  try:
108
- await client.shutdown_async(None)
115
+ await asyncio.wait_for(client.shutdown_async(None), timeout=5.0)
109
116
  client.exit(None)
110
117
  except Exception:
111
118
  pass
112
- await client.stop()
119
+ try:
120
+ await asyncio.wait_for(client.stop(), timeout=5.0)
121
+ except Exception:
122
+ pass
113
123
 
114
124
 
115
125
  async def _open_and_wait_for_diagnostics(
@@ -121,10 +131,21 @@ async def _open_and_wait_for_diagnostics(
121
131
  ) -> list[lsp.Diagnostic]:
122
132
  """Open a document and wait until publishDiagnostics arrives for its URI.
123
133
 
124
- The server publishes diagnostics asynchronously after didOpen. We poll
125
- the client's collected notifications until the URI appears or timeout.
134
+ The server publishes diagnostics asynchronously after didOpen. We register
135
+ an asyncio.Event that on_publish signals, then await it with wait_for so the
136
+ full timeout is always respected — even when the event loop is transiently
137
+ busy (e.g. jdtls subprocess creation on a slow macOS runner).
138
+
139
+ Note: the server always emits publishDiagnostics for every opened file
140
+ (including clean ones with zero findings) synchronously inside on_did_open,
141
+ before jdtls is involved. The event is therefore guaranteed to fire on the
142
+ first — and correct — notification for each URI in this test setup.
126
143
  """
127
144
  client._published.pop(uri, None) # type: ignore[attr-defined]
145
+ # Register event before sending didOpen — no await between these two lines,
146
+ # so on_publish cannot fire and miss the registration.
147
+ event = asyncio.Event()
148
+ client._diag_events[uri] = event # type: ignore[attr-defined]
128
149
 
129
150
  client.text_document_did_open(
130
151
  lsp.DidOpenTextDocumentParams(
@@ -137,14 +158,18 @@ async def _open_and_wait_for_diagnostics(
137
158
  )
138
159
  )
139
160
 
140
- deadline = asyncio.get_running_loop().time() + timeout
141
- while asyncio.get_running_loop().time() < deadline:
142
- if uri in client._published: # type: ignore[attr-defined]
143
- return client._published[uri] # type: ignore[attr-defined]
144
- await asyncio.sleep(0.1)
161
+ try:
162
+ await asyncio.wait_for(event.wait(), timeout=timeout)
163
+ except asyncio.TimeoutError:
164
+ # pytest.fail() raises Failed (a BaseException subclass);
165
+ # the finally block below still runs before the exception propagates.
166
+ pytest.fail(f"Timed out waiting for publishDiagnostics on {uri}")
167
+ finally:
168
+ client._diag_events.pop(uri, None) # type: ignore[attr-defined]
145
169
 
146
- pytest.fail(f"Timed out waiting for publishDiagnostics on {uri}")
147
- return [] # unreachable, but satisfies type checker
170
+ # on_publish stores to _published before setting the event, so the key
171
+ # is guaranteed to exist here — use direct access to surface any violation.
172
+ return client._published[uri] # type: ignore[attr-defined]
148
173
 
149
174
 
150
175
  # --------------------------------------------------------------------------
@@ -2077,7 +2102,7 @@ class TestCacheClear:
2077
2102
  # --------------------------------------------------------------------------
2078
2103
 
2079
2104
 
2080
- @pytest.mark.timeout(30)
2105
+ @pytest.mark.timeout(60) # covers fixture setup (subprocess spawn + initialize handshake) on slow macOS runners
2081
2106
  class TestLspLifecycle:
2082
2107
  """Full LSP lifecycle tests via real stdio transport — zero mocks."""
2083
2108
 
@@ -1,5 +0,0 @@
1
- {
2
- "name": "java-functional-lsp",
3
- "description": "Java LSP with functional programming rules enforcement — null safety, immutability, no exceptions, Spring best practices. Wraps jdtls for full Java language support.",
4
- "version": "0.4.1"
5
- }