java-functional-lsp 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/PKG-INFO +43 -4
  2. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/README.md +42 -3
  3. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/SKILL.md +1 -0
  4. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/pyproject.toml +1 -1
  5. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/__init__.py +1 -1
  6. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/analyzers/base.py +55 -0
  7. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/server.py +6 -1
  8. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_e2e.py +25 -0
  9. java_functional_lsp-0.5.0/tests/test_suppress.py +246 -0
  10. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/uv.lock +1 -1
  11. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.claude-plugin/plugin.json +0 -0
  12. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.githooks/pre-commit +0 -0
  13. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.githooks/pre-push +0 -0
  14. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/CODEOWNERS +0 -0
  15. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  16. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  17. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/SECURITY.md +0 -0
  19. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/dependabot.yml +0 -0
  20. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/release-drafter.yml +0 -0
  21. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/workflows/publish.yml +0 -0
  22. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/workflows/release-drafter.yml +0 -0
  23. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/workflows/stale.yml +0 -0
  24. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/workflows/test.yml +0 -0
  25. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/workflows/update-homebrew.yml +0 -0
  26. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.github/workflows/vscode-ext.yml +0 -0
  27. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/.gitignore +0 -0
  28. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/CONTRIBUTING.md +0 -0
  29. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/LICENSE +0 -0
  30. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/commands/lint-java.md +0 -0
  31. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/intellij/README.md +0 -0
  32. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/intellij/lsp4ij-template.json +0 -0
  33. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/vscode/.vscodeignore +0 -0
  34. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/vscode/README.md +0 -0
  35. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/vscode/package-lock.json +0 -0
  36. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/vscode/package.json +0 -0
  37. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/vscode/src/extension.ts +0 -0
  38. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/editors/vscode/tsconfig.json +0 -0
  39. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/hooks/hooks.json +0 -0
  40. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/hooks/java_linter_reminder.py +0 -0
  41. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/scripts/ensure-lsp.sh +0 -0
  42. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/scripts/generate-formula.py +0 -0
  43. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/__main__.py +0 -0
  44. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  45. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  46. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  47. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  48. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  49. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/cli.py +0 -0
  50. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/src/java_functional_lsp/proxy.py +0 -0
  51. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/__init__.py +0 -0
  52. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/conftest.py +0 -0
  53. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_base.py +0 -0
  54. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_cli.py +0 -0
  55. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_config.py +0 -0
  56. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_exception_checker.py +0 -0
  57. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_mutation_checker.py +0 -0
  58. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_null_checker.py +0 -0
  59. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_proxy.py +0 -0
  60. {java_functional_lsp-0.4.2 → java_functional_lsp-0.5.0}/tests/test_spring_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.4.2
3
+ Version: 0.5.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,10 +33,28 @@ Description-Content-Type: text/markdown
33
33
  [![Python](https://img.shields.io/pypi/pyversions/java-functional-lsp?v=1)](https://pypi.org/project/java-functional-lsp/)
34
34
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
35
 
36
- A Java Language Server that enforces functional programming best practices. Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
36
+ A Java Language Server that provides two things in one:
37
+
38
+ 1. **Full Java language support** — completions, hover, go-to-definition, compile errors, missing imports — by proxying [Eclipse jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) under the hood
39
+ 2. **12 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
40
+
41
+ Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
37
42
 
38
43
  ## What it checks
39
44
 
45
+ ### Java language (via jdtls)
46
+
47
+ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the server proxies all standard Java language features:
48
+
49
+ - Compile errors and warnings
50
+ - Missing imports and unresolved symbols
51
+ - Type mismatches
52
+ - Completions, hover, go-to-definition, find references
53
+
54
+ 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.
55
+
56
+ ### Functional programming rules
57
+
40
58
  | Rule | Detects | Suggests |
41
59
  |------|---------|----------|
42
60
  | `null-literal-arg` | `null` passed as method argument | `Option.none()` or default value |
@@ -64,7 +82,7 @@ pip install java-functional-lsp
64
82
  # From source
65
83
  pip install git+https://github.com/aviadshiber/java-functional-lsp.git
66
84
 
67
- # Optional: install jdtls for full Java language support (completions, hover, go-to-def)
85
+ # Optional: install jdtls for full Java language support (see above)
68
86
  brew install jdtls
69
87
  ```
70
88
 
@@ -212,9 +230,30 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
212
230
  - `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
213
231
  - `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
214
232
 
233
+ **Inline suppression** with `@SuppressWarnings`:
234
+
235
+ ```java
236
+ // Suppress a specific rule on a method
237
+ @SuppressWarnings("java-functional-lsp:null-return")
238
+ public String findUser() { return null; } // no diagnostic
239
+
240
+ // Suppress multiple rules
241
+ @SuppressWarnings({"java-functional-lsp:null-return", "java-functional-lsp:throw-statement"})
242
+ public String findUser() { ... }
243
+
244
+ // Suppress all java-functional-lsp rules
245
+ @SuppressWarnings("java-functional-lsp")
246
+ public String legacyMethod() { ... }
247
+ ```
248
+
249
+ Works on classes, methods, constructors, fields, and local variables. Suppression applies to the annotated scope — a class-level annotation suppresses all methods within it.
250
+
215
251
  ## How it works
216
252
 
217
- Uses [tree-sitter](https://tree-sitter.github.io/) with the Java grammar for fast, incremental AST parsing. No Java compiler or classpath needed — analysis runs on raw source files.
253
+ The server has two layers:
254
+
255
+ - **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.
256
+ - **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
257
 
219
258
  The server speaks the Language Server Protocol (LSP) via stdio, making it compatible with any LSP client.
220
259
 
@@ -5,10 +5,28 @@
5
5
  [![Python](https://img.shields.io/pypi/pyversions/java-functional-lsp?v=1)](https://pypi.org/project/java-functional-lsp/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- A Java Language Server that enforces functional programming best practices. Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
8
+ A Java Language Server that provides two things in one:
9
+
10
+ 1. **Full Java language support** — completions, hover, go-to-definition, compile errors, missing imports — by proxying [Eclipse jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) under the hood
11
+ 2. **12 functional programming rules** — catches anti-patterns and suggests Vavr/Lombok/Spring alternatives, all before compilation
12
+
13
+ Designed for teams using **Vavr**, **Lombok**, and **Spring** with a functional-first approach.
9
14
 
10
15
  ## What it checks
11
16
 
17
+ ### Java language (via jdtls)
18
+
19
+ When [jdtls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) is installed, the server proxies all standard Java language features:
20
+
21
+ - Compile errors and warnings
22
+ - Missing imports and unresolved symbols
23
+ - Type mismatches
24
+ - Completions, hover, go-to-definition, find references
25
+
26
+ 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.
27
+
28
+ ### Functional programming rules
29
+
12
30
  | Rule | Detects | Suggests |
13
31
  |------|---------|----------|
14
32
  | `null-literal-arg` | `null` passed as method argument | `Option.none()` or default value |
@@ -36,7 +54,7 @@ pip install java-functional-lsp
36
54
  # From source
37
55
  pip install git+https://github.com/aviadshiber/java-functional-lsp.git
38
56
 
39
- # Optional: install jdtls for full Java language support (completions, hover, go-to-def)
57
+ # Optional: install jdtls for full Java language support (see above)
40
58
  brew install jdtls
41
59
  ```
42
60
 
@@ -184,9 +202,30 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
184
202
  - `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
185
203
  - `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
186
204
 
205
+ **Inline suppression** with `@SuppressWarnings`:
206
+
207
+ ```java
208
+ // Suppress a specific rule on a method
209
+ @SuppressWarnings("java-functional-lsp:null-return")
210
+ public String findUser() { return null; } // no diagnostic
211
+
212
+ // Suppress multiple rules
213
+ @SuppressWarnings({"java-functional-lsp:null-return", "java-functional-lsp:throw-statement"})
214
+ public String findUser() { ... }
215
+
216
+ // Suppress all java-functional-lsp rules
217
+ @SuppressWarnings("java-functional-lsp")
218
+ public String legacyMethod() { ... }
219
+ ```
220
+
221
+ Works on classes, methods, constructors, fields, and local variables. Suppression applies to the annotated scope — a class-level annotation suppresses all methods within it.
222
+
187
223
  ## How it works
188
224
 
189
- Uses [tree-sitter](https://tree-sitter.github.io/) with the Java grammar for fast, incremental AST parsing. No Java compiler or classpath needed — analysis runs on raw source files.
225
+ The server has two layers:
226
+
227
+ - **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.
228
+ - **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
229
 
191
230
  The server speaks the Language Server Protocol (LSP) via stdio, making it compatible with any LSP client.
192
231
 
@@ -58,6 +58,7 @@ Create `.java-functional-lsp.json` in your project root:
58
58
  - `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
59
59
  - `throw-statement`/`catch-rethrow` auto-suppressed in `@Bean` methods
60
60
  - `mutable-dto` suggests `@ConstructorBinding` for `@ConfigurationProperties` classes
61
+ - Inline suppression: `@SuppressWarnings("java-functional-lsp:rule-id")` on any declaration
61
62
 
62
63
  ## Automatic Enforcement
63
64
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.4.2"
7
+ version = "0.5.0"
8
8
  description = "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """java-functional-lsp: A Java LSP server enforcing functional programming best practices."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.5.0"
@@ -160,6 +160,61 @@ def is_excluded(path_str: str, patterns: list[str]) -> bool:
160
160
  return any(fnmatch.fnmatch(normalized, pattern) for pattern in patterns)
161
161
 
162
162
 
163
+ _SUPPRESS_PREFIX = "java-functional-lsp"
164
+ _DECLARATION_TYPES = frozenset(
165
+ {
166
+ "method_declaration",
167
+ "class_declaration",
168
+ "interface_declaration",
169
+ "enum_declaration",
170
+ "record_declaration",
171
+ "field_declaration",
172
+ "local_variable_declaration",
173
+ "constructor_declaration",
174
+ }
175
+ )
176
+
177
+
178
+ def is_suppressed(root: Node, line: int, col: int, rule_id: str) -> bool:
179
+ """Check if a diagnostic at (line, col) is suppressed by @SuppressWarnings."""
180
+ node = root.descendant_for_point_range((line, col), (line, col))
181
+ if node is None:
182
+ return False
183
+ current: Node | None = node
184
+ while current is not None:
185
+ if current.type in _DECLARATION_TYPES:
186
+ modifiers = next((c for c in current.children if c.type == "modifiers"), None)
187
+ if modifiers and _modifiers_suppress(modifiers, rule_id):
188
+ return True
189
+ current = current.parent
190
+ return False
191
+
192
+
193
+ def _modifiers_suppress(modifiers: Node, rule_id: str) -> bool:
194
+ """Check if modifiers contain @SuppressWarnings suppressing the given rule."""
195
+ for child in modifiers.named_children:
196
+ if child.type == "annotation":
197
+ name_node = child.child_by_field_name("name")
198
+ if name_node and name_node.text == b"SuppressWarnings":
199
+ args = child.child_by_field_name("arguments")
200
+ if args and _annotation_args_suppress(args, rule_id):
201
+ return True
202
+ return False
203
+
204
+
205
+ def _annotation_args_suppress(args: Node, rule_id: str) -> bool:
206
+ """Parse @SuppressWarnings value(s) and check for rule match."""
207
+ for string_node in find_nodes(args, "string_literal"):
208
+ if string_node.text is None or len(string_node.text) < len('""'):
209
+ continue
210
+ value = string_node.text[1:-1].decode("utf-8")
211
+ if value == _SUPPRESS_PREFIX:
212
+ return True
213
+ if value == f"{_SUPPRESS_PREFIX}:{rule_id}":
214
+ return True
215
+ return False
216
+
217
+
163
218
  def has_sibling_annotation(modifiers_node: Node, annotation_name: bytes) -> bool:
164
219
  """Check if a modifiers node contains an annotation with the given name.
165
220
 
@@ -18,7 +18,7 @@ from lsprotocol import types as lsp
18
18
  from pygls.lsp.server import LanguageServer
19
19
  from pygls.uris import to_fs_path
20
20
 
21
- from .analyzers.base import Analyzer, Severity, get_parser, is_excluded
21
+ from .analyzers.base import Analyzer, Severity, get_parser, is_excluded, is_suppressed
22
22
  from .analyzers.base import Diagnostic as LintDiagnostic
23
23
  from .analyzers.exception_checker import ExceptionChecker
24
24
  from .analyzers.mutation_checker import MutationChecker
@@ -124,6 +124,11 @@ def _analyze_document(source_text: str, uri: str = "") -> list[lsp.Diagnostic]:
124
124
  except Exception as e:
125
125
  logger.error("Analyzer %s failed: %s", type(analyzer).__name__, e)
126
126
 
127
+ # Filter out diagnostics suppressed by @SuppressWarnings
128
+ if all_diagnostics:
129
+ root = tree.root_node
130
+ all_diagnostics = [d for d in all_diagnostics if not is_suppressed(root, d.line, d.col, d.code)]
131
+
127
132
  return [_to_lsp_diagnostic(d) for d in all_diagnostics]
128
133
 
129
134
 
@@ -349,3 +349,28 @@ class TestE2EConfig:
349
349
  msg = _wait_diagnostics(server)
350
350
  assert msg is not None
351
351
  assert len(msg["params"]["diagnostics"]) == 0
352
+
353
+
354
+ class TestE2ESuppressWarnings:
355
+ def test_suppress_warnings_annotation(self, server: subprocess.Popen[bytes], tmp_path: Path) -> None:
356
+ """@SuppressWarnings should suppress diagnostics for the annotated scope."""
357
+ java_file = tmp_path / "Suppress.java"
358
+ java_file.write_text(
359
+ "class T {\n"
360
+ ' @SuppressWarnings("java-functional-lsp:null-return")\n'
361
+ " String f() { return null; }\n"
362
+ "\n"
363
+ " String g() { return null; }\n"
364
+ "}"
365
+ )
366
+ uri = java_file.as_uri()
367
+
368
+ _initialize(server, root_uri=tmp_path.as_uri())
369
+ _did_open(server, uri, java_file.read_text())
370
+
371
+ msg = _wait_diagnostics(server)
372
+ assert msg is not None
373
+ null_diags = [d for d in msg["params"]["diagnostics"] if d["code"] == "null-return"]
374
+ # f() suppressed, g() not — should have exactly 1 null-return diagnostic on line 4 (g)
375
+ assert len(null_diags) == 1
376
+ assert null_diags[0]["range"]["start"]["line"] == 4
@@ -0,0 +1,246 @@
1
+ """Tests for @SuppressWarnings inline suppression."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from java_functional_lsp.analyzers.base import get_parser, is_suppressed
8
+
9
+
10
+ def _codes(diags: list[Any]) -> set[str]:
11
+ return {d.code for d in diags}
12
+
13
+
14
+ def _analyze_all(source: str) -> list[Any]:
15
+ """Run analysis through server's _analyze_document to include suppression filtering."""
16
+ from java_functional_lsp.server import _analyze_document
17
+
18
+ return _analyze_document(source)
19
+
20
+
21
+ class TestSuppressSingleRule:
22
+ def test_suppress_null_return(self) -> None:
23
+ source = """
24
+ class T {
25
+ @SuppressWarnings("java-functional-lsp:null-return")
26
+ String f() { return null; }
27
+ }
28
+ """
29
+ diags = _analyze_all(source)
30
+ assert "null-return" not in _codes(diags)
31
+
32
+ def test_suppress_throw_statement(self) -> None:
33
+ source = """
34
+ class T {
35
+ @SuppressWarnings("java-functional-lsp:throw-statement")
36
+ void f() { throw new RuntimeException(); }
37
+ }
38
+ """
39
+ diags = _analyze_all(source)
40
+ assert "throw-statement" not in _codes(diags)
41
+
42
+
43
+ class TestSuppressAllRules:
44
+ def test_suppress_all(self) -> None:
45
+ source = """
46
+ class T {
47
+ @SuppressWarnings("java-functional-lsp")
48
+ String f() { return null; }
49
+ }
50
+ """
51
+ diags = _analyze_all(source)
52
+ assert "null-return" not in _codes(diags)
53
+
54
+ def test_suppress_all_multiple_violations(self) -> None:
55
+ source = """
56
+ class T {
57
+ @SuppressWarnings("java-functional-lsp")
58
+ void f() {
59
+ String x = null;
60
+ throw new RuntimeException();
61
+ }
62
+ }
63
+ """
64
+ diags = _analyze_all(source)
65
+ codes = _codes(diags)
66
+ assert "null-assignment" not in codes
67
+ assert "throw-statement" not in codes
68
+
69
+
70
+ class TestSuppressMultipleRules:
71
+ def test_suppress_array_syntax(self) -> None:
72
+ source = """
73
+ class T {
74
+ @SuppressWarnings({"java-functional-lsp:null-return", "java-functional-lsp:throw-statement"})
75
+ String f() {
76
+ if (true) throw new RuntimeException();
77
+ return null;
78
+ }
79
+ }
80
+ """
81
+ diags = _analyze_all(source)
82
+ codes = _codes(diags)
83
+ assert "null-return" not in codes
84
+ assert "throw-statement" not in codes
85
+
86
+
87
+ class TestSuppressOnClass:
88
+ def test_class_level_suppresses_all_methods(self) -> None:
89
+ source = """
90
+ @SuppressWarnings("java-functional-lsp:null-return")
91
+ class T {
92
+ String f() { return null; }
93
+ String g() { return null; }
94
+ }
95
+ """
96
+ diags = _analyze_all(source)
97
+ assert "null-return" not in _codes(diags)
98
+
99
+
100
+ class TestSuppressOnField:
101
+ def test_field_suppression(self) -> None:
102
+ source = """
103
+ class T {
104
+ @SuppressWarnings("java-functional-lsp:null-field-assignment")
105
+ String cache = null;
106
+ }
107
+ """
108
+ diags = _analyze_all(source)
109
+ assert "null-field-assignment" not in _codes(diags)
110
+
111
+
112
+ class TestSuppressScope:
113
+ def test_does_not_affect_sibling_methods(self) -> None:
114
+ source = """
115
+ class T {
116
+ @SuppressWarnings("java-functional-lsp:null-return")
117
+ String f() { return null; }
118
+
119
+ String g() { return null; }
120
+ }
121
+ """
122
+ diags = _analyze_all(source)
123
+ # f() is suppressed, g() is not
124
+ assert any(d.code == "null-return" for d in diags)
125
+ # Verify the remaining diagnostic is on g()'s line
126
+ null_diags = [d for d in diags if d.code == "null-return"]
127
+ assert len(null_diags) == 1
128
+
129
+
130
+ class TestUnrelatedSuppressIgnored:
131
+ def test_unrelated_annotation(self) -> None:
132
+ source = """
133
+ class T {
134
+ @SuppressWarnings("unchecked")
135
+ String f() { return null; }
136
+ }
137
+ """
138
+ diags = _analyze_all(source)
139
+ assert "null-return" in _codes(diags)
140
+
141
+ def test_other_prefix_ignored(self) -> None:
142
+ source = """
143
+ class T {
144
+ @SuppressWarnings("other-linter:null-return")
145
+ String f() { return null; }
146
+ }
147
+ """
148
+ diags = _analyze_all(source)
149
+ assert "null-return" in _codes(diags)
150
+
151
+
152
+ class TestSuppressOnInterface:
153
+ def test_interface_level_suppression(self) -> None:
154
+ source = """
155
+ @SuppressWarnings("java-functional-lsp:null-return")
156
+ interface Foo {
157
+ default String bar() { return null; }
158
+ }
159
+ """
160
+ diags = _analyze_all(source)
161
+ assert "null-return" not in _codes(diags)
162
+
163
+
164
+ class TestSuppressOnEnum:
165
+ def test_enum_level_suppression(self) -> None:
166
+ source = """
167
+ @SuppressWarnings("java-functional-lsp:null-return")
168
+ enum Status {
169
+ ACTIVE;
170
+ public String label() { return null; }
171
+ }
172
+ """
173
+ diags = _analyze_all(source)
174
+ assert "null-return" not in _codes(diags)
175
+
176
+
177
+ class TestSuppressSiblingClasses:
178
+ def test_does_not_leak_across_top_level_classes(self) -> None:
179
+ source = """
180
+ @SuppressWarnings("java-functional-lsp:null-return")
181
+ class A { String f() { return null; } }
182
+ class B { String g() { return null; } }
183
+ """
184
+ diags = _analyze_all(source)
185
+ null_diags = [d for d in diags if d.code == "null-return"]
186
+ assert len(null_diags) == 1
187
+
188
+
189
+ class TestSuppressNamedElement:
190
+ def test_value_equals_form(self) -> None:
191
+ source = """
192
+ class T {
193
+ @SuppressWarnings(value = "java-functional-lsp:null-return")
194
+ String f() { return null; }
195
+ }
196
+ """
197
+ diags = _analyze_all(source)
198
+ assert "null-return" not in _codes(diags)
199
+
200
+
201
+ class TestSuppressOtherRules:
202
+ def test_suppress_imperative_loop(self) -> None:
203
+ source = """
204
+ class T {
205
+ @SuppressWarnings("java-functional-lsp:imperative-loop")
206
+ void f() { for (int i = 0; i < 10; i++) {} }
207
+ }
208
+ """
209
+ diags = _analyze_all(source)
210
+ assert "imperative-loop" not in _codes(diags)
211
+
212
+ def test_suppress_mutable_variable(self) -> None:
213
+ source = """
214
+ class T {
215
+ @SuppressWarnings("java-functional-lsp:mutable-variable")
216
+ void f() { int x = 0; x = 1; }
217
+ }
218
+ """
219
+ diags = _analyze_all(source)
220
+ assert "mutable-variable" not in _codes(diags)
221
+
222
+
223
+ class TestNoSuppress:
224
+ def test_baseline_no_annotation(self) -> None:
225
+ source = "class T { String f() { return null; } }"
226
+ diags = _analyze_all(source)
227
+ assert "null-return" in _codes(diags)
228
+
229
+
230
+ class TestIsSuppressedHelper:
231
+ def test_returns_false_for_clean_code(self) -> None:
232
+ parser = get_parser()
233
+ tree = parser.parse(b'class T { String f() { return "ok"; } }')
234
+ assert not is_suppressed(tree.root_node, 0, 30, "null-return")
235
+
236
+ def test_returns_true_for_suppressed(self) -> None:
237
+ parser = get_parser()
238
+ source = b"""class T {
239
+ @SuppressWarnings("java-functional-lsp:null-return")
240
+ String f() { return null; }
241
+ }"""
242
+ tree = parser.parse(source)
243
+ # null is on line 2, find its column
244
+ lines = source.split(b"\n")
245
+ null_col = lines[2].index(b"null")
246
+ assert is_suppressed(tree.root_node, 2, null_col, "null-return")
@@ -184,7 +184,7 @@ wheels = [
184
184
 
185
185
  [[package]]
186
186
  name = "java-functional-lsp"
187
- version = "0.4.1"
187
+ version = "0.4.2"
188
188
  source = { editable = "." }
189
189
  dependencies = [
190
190
  { name = "pygls" },