java-functional-lsp 0.3.2__tar.gz → 0.4.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 (55) hide show
  1. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.claude-plugin/plugin.json +1 -1
  2. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/PKG-INFO +9 -3
  3. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/README.md +8 -2
  4. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/SKILL.md +5 -1
  5. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/vscode/package.json +1 -1
  6. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/pyproject.toml +1 -1
  7. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/__init__.py +1 -1
  8. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/analyzers/base.py +24 -0
  9. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/analyzers/exception_checker.py +18 -2
  10. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/analyzers/mutation_checker.py +10 -3
  11. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/cli.py +4 -1
  12. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/server.py +8 -1
  13. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_base.py +35 -0
  14. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_exception_checker.py +42 -0
  15. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_mutation_checker.py +14 -0
  16. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/CODEOWNERS +0 -0
  17. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  18. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  19. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/SECURITY.md +0 -0
  21. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/dependabot.yml +0 -0
  22. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/release-drafter.yml +0 -0
  23. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/workflows/publish.yml +0 -0
  24. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/workflows/release-drafter.yml +0 -0
  25. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/workflows/stale.yml +0 -0
  26. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/workflows/test.yml +0 -0
  27. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/workflows/update-homebrew.yml +0 -0
  28. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.github/workflows/vscode-ext.yml +0 -0
  29. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/.gitignore +0 -0
  30. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/CONTRIBUTING.md +0 -0
  31. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/LICENSE +0 -0
  32. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/commands/lint-java.md +0 -0
  33. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/intellij/README.md +0 -0
  34. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/intellij/lsp4ij-template.json +0 -0
  35. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/vscode/.vscodeignore +0 -0
  36. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/vscode/README.md +0 -0
  37. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/vscode/package-lock.json +0 -0
  38. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/vscode/src/extension.ts +0 -0
  39. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/editors/vscode/tsconfig.json +0 -0
  40. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/hooks/hooks.json +0 -0
  41. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/hooks/java_linter_reminder.py +0 -0
  42. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/scripts/ensure-lsp.sh +0 -0
  43. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/scripts/generate-formula.py +0 -0
  44. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  45. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  46. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  47. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/src/java_functional_lsp/proxy.py +0 -0
  48. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/__init__.py +0 -0
  49. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/conftest.py +0 -0
  50. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_cli.py +0 -0
  51. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_config.py +0 -0
  52. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_null_checker.py +0 -0
  53. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_proxy.py +0 -0
  54. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/tests/test_spring_checker.py +0 -0
  55. {java_functional_lsp-0.3.2 → java_functional_lsp-0.4.0}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "java-functional-lsp",
3
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.3.2"
4
+ "version": "0.4.0"
5
5
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.3.2
3
+ Version: 0.4.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
@@ -146,6 +146,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
146
146
 
147
147
  ```json
148
148
  {
149
+ "excludes": ["**/generated/**", "**/vendor/**"],
149
150
  "rules": {
150
151
  "null-literal-arg": "warning",
151
152
  "throw-statement": "info",
@@ -155,8 +156,13 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
155
156
  }
156
157
  ```
157
158
 
158
- Severity levels: `error`, `warning`, `info`, `hint`, `off`.
159
- All rules default to `warning` when not configured.
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`
160
166
 
161
167
  ## How it works
162
168
 
@@ -118,6 +118,7 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
118
118
 
119
119
  ```json
120
120
  {
121
+ "excludes": ["**/generated/**", "**/vendor/**"],
121
122
  "rules": {
122
123
  "null-literal-arg": "warning",
123
124
  "throw-statement": "info",
@@ -127,8 +128,13 @@ Create `.java-functional-lsp.json` in your project root to customize rules:
127
128
  }
128
129
  ```
129
130
 
130
- Severity levels: `error`, `warning`, `info`, `hint`, `off`.
131
- All rules default to `warning` when not configured.
131
+ **Options:**
132
+ - `excludes` glob patterns for files/directories to skip entirely (supports `**` for multi-segment wildcards)
133
+ - `rules` — per-rule severity: `error`, `warning` (default), `info`, `hint`, `off`
134
+
135
+ **Spring-aware behavior:**
136
+ - `throw-statement` and `catch-rethrow` are automatically suppressed inside `@Bean` methods
137
+ - `mutable-dto` suggests `@ConstructorBinding` instead of `@Value` when the class has `@ConfigurationProperties`
132
138
 
133
139
  ## How it works
134
140
 
@@ -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
- Severity levels: `error`, `warning` (default), `info`, `hint`, `off`.
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
 
@@ -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.3.2",
5
+ "version": "0.4.0",
6
6
  "publisher": "aviadshiber",
7
7
  "license": "MIT",
8
8
  "engines": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.3.2"
7
+ version = "0.4.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.3.2"
3
+ __version__ = "0.4.0"
@@ -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
- grandparent = node.parent.parent
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=_MESSAGES["mutable-dto"],
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))
@@ -16,7 +16,7 @@ import cattrs
16
16
  from lsprotocol import types as lsp
17
17
  from pygls.lsp.server import LanguageServer
18
18
 
19
- from .analyzers.base import Analyzer, Severity, get_parser
19
+ from .analyzers.base import Analyzer, Severity, get_parser, is_excluded
20
20
  from .analyzers.base import Diagnostic as LintDiagnostic
21
21
  from .analyzers.exception_checker import ExceptionChecker
22
22
  from .analyzers.mutation_checker import MutationChecker
@@ -96,6 +96,13 @@ def _to_lsp_diagnostic(diag: LintDiagnostic) -> lsp.Diagnostic:
96
96
 
97
97
  def _analyze_document(source_text: str, uri: str = "") -> list[lsp.Diagnostic]:
98
98
  """Run all custom analyzers on the given source text. Uses incremental parsing when possible."""
99
+ # Check excludes before parsing
100
+ if uri:
101
+ excludes: list[str] = server._config.get("excludes", [])
102
+ if excludes:
103
+ path_str = _uri_to_path(uri)
104
+ if is_excluded(path_str, excludes):
105
+ return []
99
106
  source_bytes = source_text.encode("utf-8")
100
107
  old_tree = server._trees.get(uri) if uri else None
101
108
  tree = server._parser.parse(source_bytes, old_tree) if old_tree else server._parser.parse(source_bytes)
@@ -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: