python-checks 0.1.0__py3-none-any.whl

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 (101) hide show
  1. py_checks/__init__.py +2 -0
  2. py_checks/checks/__init__.py +20 -0
  3. py_checks/checks/_kind.py +184 -0
  4. py_checks/checks/_location.py +172 -0
  5. py_checks/checks/_names.py +86 -0
  6. py_checks/checks/api/__init__.py +13 -0
  7. py_checks/checks/api/_endpoint_declarations.py +204 -0
  8. py_checks/checks/api/_marker.py +5 -0
  9. py_checks/checks/calls/__init__.py +13 -0
  10. py_checks/checks/calls/_confined_functions.py +102 -0
  11. py_checks/checks/calls/_marker.py +5 -0
  12. py_checks/checks/database/__init__.py +39 -0
  13. py_checks/checks/database/_bound_checks.py +186 -0
  14. py_checks/checks/database/_confined_calls.py +115 -0
  15. py_checks/checks/database/_marker.py +5 -0
  16. py_checks/checks/database/_model_boundary.py +236 -0
  17. py_checks/checks/database/_model_columns.py +241 -0
  18. py_checks/checks/database/_raw_sql.py +108 -0
  19. py_checks/checks/database/_schema_drift.py +271 -0
  20. py_checks/checks/database/_statement_keys.py +169 -0
  21. py_checks/checks/effects/__init__.py +19 -0
  22. py_checks/checks/effects/_determinism.py +105 -0
  23. py_checks/checks/effects/_log_events.py +120 -0
  24. py_checks/checks/effects/_marker.py +5 -0
  25. py_checks/checks/hygiene/__init__.py +14 -0
  26. py_checks/checks/hygiene/_dependency_bounds.py +185 -0
  27. py_checks/checks/hygiene/_marker.py +5 -0
  28. py_checks/checks/imports/__init__.py +25 -0
  29. py_checks/checks/imports/_confined.py +93 -0
  30. py_checks/checks/imports/_marker.py +7 -0
  31. py_checks/checks/imports/_sealed.py +100 -0
  32. py_checks/checks/imports/_statements.py +52 -0
  33. py_checks/checks/placement/__init__.py +38 -0
  34. py_checks/checks/placement/_class_modules.py +106 -0
  35. py_checks/checks/placement/_class_placement.py +129 -0
  36. py_checks/checks/placement/_marker.py +7 -0
  37. py_checks/checks/placement/_operation_shape.py +387 -0
  38. py_checks/checks/placement/_required_class.py +179 -0
  39. py_checks/checks/signatures/__init__.py +33 -0
  40. py_checks/checks/signatures/_function_length.py +90 -0
  41. py_checks/checks/signatures/_functions.py +92 -0
  42. py_checks/checks/signatures/_keyword_only.py +148 -0
  43. py_checks/checks/signatures/_marker.py +7 -0
  44. py_checks/checks/signatures/_module_length.py +64 -0
  45. py_checks/checks/signatures/_nesting.py +156 -0
  46. py_checks/checks/signatures/_signature_layout.py +231 -0
  47. py_checks/checks/types/__init__.py +36 -0
  48. py_checks/checks/types/_annotation_shapes.py +127 -0
  49. py_checks/checks/types/_config_fields.py +236 -0
  50. py_checks/checks/types/_confined_types.py +117 -0
  51. py_checks/checks/types/_constant_annotations.py +128 -0
  52. py_checks/checks/types/_frozen_dataclasses.py +112 -0
  53. py_checks/checks/types/_marker.py +5 -0
  54. py_checks/cli/__init__.py +10 -0
  55. py_checks/cli/_app.py +21 -0
  56. py_checks/cli/_protocols.py +19 -0
  57. py_checks/cli/commands/__init__.py +23 -0
  58. py_checks/cli/commands/_explain.py +28 -0
  59. py_checks/cli/commands/_list.py +62 -0
  60. py_checks/cli/commands/_run.py +167 -0
  61. py_checks/cli/commands/_summary.py +20 -0
  62. py_checks/cli/commands/_sync.py +80 -0
  63. py_checks/config/__init__.py +31 -0
  64. py_checks/config/_base.py +26 -0
  65. py_checks/config/_config.py +76 -0
  66. py_checks/config/_constants.py +31 -0
  67. py_checks/config/_errors.py +12 -0
  68. py_checks/config/_loader.py +133 -0
  69. py_checks/config/_toml.py +24 -0
  70. py_checks/contracts/__init__.py +26 -0
  71. py_checks/contracts/_constants.py +18 -0
  72. py_checks/contracts/_layout.py +63 -0
  73. py_checks/contracts/_render.py +217 -0
  74. py_checks/contracts/_settings.py +37 -0
  75. py_checks/core/__init__.py +56 -0
  76. py_checks/core/_constants.py +15 -0
  77. py_checks/core/_discovery.py +57 -0
  78. py_checks/core/_edit.py +92 -0
  79. py_checks/core/_errors.py +35 -0
  80. py_checks/core/_fixer.py +55 -0
  81. py_checks/core/_format.py +30 -0
  82. py_checks/core/_markers.py +220 -0
  83. py_checks/core/_protocols.py +88 -0
  84. py_checks/core/_registry.py +77 -0
  85. py_checks/core/_report.py +45 -0
  86. py_checks/core/_runner.py +178 -0
  87. py_checks/core/_settings.py +44 -0
  88. py_checks/core/_source.py +94 -0
  89. py_checks/core/_violation.py +73 -0
  90. py_checks/environment/__init__.py +14 -0
  91. py_checks/environment/_constants.py +9 -0
  92. py_checks/environment/_render.py +227 -0
  93. py_checks/environment/_settings.py +35 -0
  94. py_checks/py.typed +0 -0
  95. py_checks/sync/__init__.py +16 -0
  96. py_checks/sync/_sync.py +60 -0
  97. python_checks-0.1.0.dist-info/METADATA +327 -0
  98. python_checks-0.1.0.dist-info/RECORD +101 -0
  99. python_checks-0.1.0.dist-info/WHEEL +4 -0
  100. python_checks-0.1.0.dist-info/entry_points.txt +33 -0
  101. python_checks-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,129 @@
1
+ """Класс лежит там, где лежат классы его вида."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, ClassVar, Final, Self
6
+
7
+ from pydantic import model_validator
8
+
9
+ from py_checks.checks._kind import Kind, declarations
10
+ from py_checks.checks._location import place
11
+ from py_checks.checks.placement._marker import MARKER
12
+ from py_checks.config import CheckSettings
13
+ from py_checks.core import Scope, Violation, settings_as
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Iterator
17
+
18
+ from py_checks.checks._kind import Declaration
19
+ from py_checks.checks._location import Place
20
+ from py_checks.core import ParsedFile
21
+
22
+ CODE: Final = "class-placement"
23
+
24
+
25
+ class Rule(CheckSettings):
26
+ """Кому куда: вид объявления или суффикс имени — и где ему место.
27
+
28
+ `area` сужает правило до части дерева. Без него `dataclass` пришлось бы
29
+ держать в `dto/` по всему сервису, а доменный value object — тоже
30
+ dataclass, и живёт он в домене.
31
+
32
+ `inside` перечисляет равноправные адреса: `errors` и `exceptions` — это
33
+ директория и модуль, и словарь отказов законно лежит в любом из них.
34
+ """
35
+
36
+ kind: Kind | None = None
37
+ suffix: str | None = None
38
+ inside: tuple[str, ...]
39
+ area: str | None = None
40
+
41
+ @model_validator(mode="after")
42
+ def _one_subject(self) -> Self:
43
+ if (self.kind is None) == (self.suffix is None):
44
+ message = "правилу нужен ровно один признак: `kind` или `suffix`"
45
+ raise ValueError(message)
46
+ return self
47
+
48
+ def about(self, *, declared: Declaration) -> bool:
49
+ if self.suffix is not None:
50
+ return declared.name.endswith(self.suffix)
51
+ return declared.kind is self.kind
52
+
53
+ @property
54
+ def said(self) -> str:
55
+ if self.suffix is not None:
56
+ return f"кончается на {self.suffix}"
57
+ return f"— {self.kind.said}" if self.kind is not None else ""
58
+
59
+
60
+ class ClassPlacementSettings(CheckSettings):
61
+ rules: tuple[Rule, ...] = ()
62
+
63
+
64
+ class ClassPlacement:
65
+ """Падает, если класс лежит не там, где лежат классы его вида.
66
+
67
+ Директория называет вид, и читатель находит порт, не открывая файла.
68
+ Правила проверяются по порядку, первое подошедшее отвечает за объявление:
69
+ исключение — это исключение, даже если его имя кончается на `Service`.
70
+
71
+ Область (`area`) — половина смысла: `dataclass` обязан лежать в `dto/`
72
+ только внутри `application`, потому что доменный value object — тоже
73
+ dataclass, и живёт он в домене.
74
+
75
+ Настройка: `rules`.
76
+ """
77
+
78
+ code: ClassVar[str] = CODE
79
+ Settings: ClassVar[type[CheckSettings]] = ClassPlacementSettings
80
+ scope: ClassVar[Scope] = Scope.FILE
81
+ marker: ClassVar[str] = MARKER
82
+
83
+ @classmethod
84
+ def run(
85
+ cls,
86
+ *,
87
+ file: ParsedFile,
88
+ settings: CheckSettings,
89
+ ) -> Iterator[Violation]:
90
+ rules = settings_as(
91
+ settings=settings,
92
+ model=ClassPlacementSettings,
93
+ code=CODE,
94
+ ).rules
95
+ where = place(file=file)
96
+ if where is None:
97
+ return
98
+ for declared in declarations(tree=file.tree):
99
+ rule = cls._rule(
100
+ declared=declared,
101
+ where=where,
102
+ rules=rules,
103
+ )
104
+ if rule is None:
105
+ continue
106
+ yield Violation.from_node(
107
+ node=declared.node,
108
+ path=file.path,
109
+ code=CODE,
110
+ message=(f"{declared.name} {rule.said}; ему место в {', '.join(rule.inside)}"),
111
+ )
112
+
113
+ @staticmethod
114
+ def _rule(
115
+ *,
116
+ declared: Declaration,
117
+ where: Place,
118
+ rules: tuple[Rule, ...],
119
+ ) -> Rule | None:
120
+ """Первое правило, которое про это объявление и которое нарушено."""
121
+ for rule in rules:
122
+ if not rule.about(declared=declared):
123
+ continue
124
+ if rule.area is not None and not where.holds(path=rule.area):
125
+ continue
126
+ if any(where.holds(path=address) for address in rule.inside):
127
+ return None
128
+ return rule
129
+ return None
@@ -0,0 +1,7 @@
1
+ """Слово, которым снимается любая проверка этой группы."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ MARKER: Final = "# placement-ok"
@@ -0,0 +1,387 @@
1
+ """Операция — один класс, одна дверь и ничего рядом."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from typing import TYPE_CHECKING, ClassVar, Final
7
+
8
+ from pydantic import Field
9
+
10
+ from py_checks.checks._kind import Kind, declarations
11
+ from py_checks.checks._location import place
12
+ from py_checks.checks.placement._marker import MARKER
13
+ from py_checks.config import CheckSettings
14
+ from py_checks.core import Scope, Violation, settings_as
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterator
18
+
19
+ from py_checks.checks._kind import Declaration
20
+ from py_checks.checks._location import Place
21
+ from py_checks.core import ParsedFile
22
+
23
+ CODE: Final = "operation-shape"
24
+
25
+ PRIVATE: Final = "_"
26
+
27
+ STATIC: Final = "staticmethod"
28
+
29
+
30
+ class Operation(CheckSettings):
31
+ """Директория с операциями и форма, которую там держат.
32
+
33
+ `method` — единственная публичная дверь: сценарий просят об одном деле, и
34
+ второй публичный метод означает вторую операцию, поделившую с первой
35
+ конструктор. Пусто — значит число дверей не ограничено: у сервиса модуля
36
+ их столько, сколько переходов у его сущности, и это тот же выбор, а не
37
+ поблажка.
38
+
39
+ `forbids` — имена типов, которых операция не держит: `UnitOfWork` ловится
40
+ и как `IPlacementUnitOfWork`, и как `UnitOfWorkFactory`, потому что
41
+ запрещено держать транзакцию, а не писать её имя одним конкретным образом.
42
+
43
+ `max_arguments` — сколько аргументов занимает вход. Дверь несёт то, что
44
+ пришло снаружи, и вход длиннее нескольких полей — вещь с именем: команда,
45
+ запрос, DTO. Считаются публичные методы: конструктор получает зависимости,
46
+ а это проводка, не вход, и в счёт он не идёт уже потому, что публичным не
47
+ является.
48
+ """
49
+
50
+ inside: str
51
+ suffix: str
52
+ method: str | None = None
53
+ forbids: tuple[str, ...] = ()
54
+ max_arguments: int | None = Field(
55
+ default=None,
56
+ gt=0,
57
+ )
58
+
59
+
60
+ class OperationShapeSettings(CheckSettings):
61
+ operations: tuple[Operation, ...] = ()
62
+
63
+
64
+ class OperationShape:
65
+ """Падает, если операция устроена не как операция.
66
+
67
+ Вход двери ограничен по числу аргументов, если предел задан: то, что
68
+ пришло снаружи, длиннее нескольких полей — это команда, запрос или DTO.
69
+
70
+ Рядом с операцией не стоит ничего: ни второй класс, ни функция — ни выше,
71
+ ни ниже. Хелпер перед предметом — абзац, который читатель пролистывает;
72
+ хелпер после — тот же хелпер, ничем не разделяемый: нужен операции — стал
73
+ приватным методом, нужен двоим — переехал туда, где лежит общее.
74
+
75
+ Константы и алиасы стоять могут: имя читают там, где им пользуются.
76
+ Перечисление — не может, в отличие от других директорий: словарь — это
77
+ класс, и операция, которой он понадобился, называет то, чем её модуль не
78
+ владеет.
79
+
80
+ `__init__.py`, пустой модуль и модуль с подчёркиванием правилу не подсудны.
81
+ Модуль, не объявивший операции вовсе, — тоже: об этом говорит
82
+ `required-class`, и второе мнение сообщило бы одну ошибку дважды.
83
+
84
+ Настройка: `operations`.
85
+ """
86
+
87
+ code: ClassVar[str] = CODE
88
+ Settings: ClassVar[type[CheckSettings]] = OperationShapeSettings
89
+ scope: ClassVar[Scope] = Scope.FILE
90
+ marker: ClassVar[str] = MARKER
91
+
92
+ @classmethod
93
+ def run(
94
+ cls,
95
+ *,
96
+ file: ParsedFile,
97
+ settings: CheckSettings,
98
+ ) -> Iterator[Violation]:
99
+ listed = settings_as(
100
+ settings=settings,
101
+ model=OperationShapeSettings,
102
+ code=CODE,
103
+ ).operations
104
+ where = place(file=file)
105
+ if where is None or file.path.stem.startswith(PRIVATE):
106
+ return
107
+ rule = cls._rule(
108
+ where=where,
109
+ listed=listed,
110
+ )
111
+ if rule is None:
112
+ return
113
+ declared = list(declarations(tree=file.tree))
114
+ subject = cls._subject(
115
+ declared=declared,
116
+ rule=rule,
117
+ )
118
+ found = list(
119
+ cls._beside(
120
+ file=file,
121
+ declared=declared,
122
+ subject=subject,
123
+ rule=rule,
124
+ )
125
+ )
126
+ if subject is not None and isinstance(subject.node, ast.ClassDef):
127
+ found += [
128
+ *cls._door(
129
+ file=file,
130
+ subject=subject,
131
+ node=subject.node,
132
+ rule=rule,
133
+ ),
134
+ *cls._input(
135
+ file=file,
136
+ subject=subject,
137
+ node=subject.node,
138
+ rule=rule,
139
+ ),
140
+ *cls._held(
141
+ file=file,
142
+ subject=subject,
143
+ node=subject.node,
144
+ rule=rule,
145
+ ),
146
+ ]
147
+ yield from sorted(found, key=lambda violation: (violation.line, violation.column))
148
+
149
+ @staticmethod
150
+ def _rule(
151
+ *,
152
+ where: Place,
153
+ listed: tuple[Operation, ...],
154
+ ) -> Operation | None:
155
+ """Правило этой директории: самое глубокое, а при равенстве — точное.
156
+
157
+ Директорий в `inside` у правила может быть несколько, и файл попадает
158
+ под оба: `use_cases` внутри `application/services` судит то, что
159
+ названо длиннее и лежит ближе.
160
+ """
161
+ matched = [
162
+ (depth, len(one.inside), one)
163
+ for one in listed
164
+ if (depth := where.within(directory=one.inside)) is not None
165
+ ]
166
+ if not matched:
167
+ return None
168
+ return max(matched, key=lambda found: found[:2])[2]
169
+
170
+ @staticmethod
171
+ def _subject(
172
+ *,
173
+ declared: list[Declaration],
174
+ rule: Operation,
175
+ ) -> Declaration | None:
176
+ """Операция, ради которой существует модуль, — по имени, а не по месту.
177
+
178
+ По месту было бы неверно: класс, по ошибке вставший выше операции, —
179
+ то самое, о чём правило и сообщает, — оказался бы предметом, и модуль
180
+ услышал бы, что у его словаря нет `execute()`. Одна ошибка — одна
181
+ жалоба.
182
+ """
183
+ classes = [one for one in declared if isinstance(one.node, ast.ClassDef)]
184
+ named = [one for one in classes if one.name.endswith(rule.suffix)]
185
+ return next(iter(named or classes), None)
186
+
187
+ @classmethod
188
+ def _beside(
189
+ cls,
190
+ *,
191
+ file: ParsedFile,
192
+ declared: list[Declaration],
193
+ subject: Declaration | None,
194
+ rule: Operation,
195
+ ) -> Iterator[Violation]:
196
+ """Всё, что встало рядом с операцией: второй класс или функция."""
197
+ for one in declared:
198
+ if one.kind is Kind.ALIAS or one is subject:
199
+ continue
200
+ if one.kind is Kind.FUNCTION:
201
+ yield cls._says(
202
+ file=file,
203
+ declared=one,
204
+ message=(
205
+ f"{one.name}() стоит рядом с операцией; нужный ей хелпер — "
206
+ f"приватный метод, нужный двоим — общий код"
207
+ ),
208
+ )
209
+ continue
210
+ yield cls._says(
211
+ file=file,
212
+ declared=one,
213
+ message=(
214
+ f"{one.name} стоит рядом с операцией; в {rule.inside} модуль "
215
+ f"объявляет один класс и больше ничего"
216
+ ),
217
+ )
218
+
219
+ @classmethod
220
+ def _door(
221
+ cls,
222
+ *,
223
+ file: ParsedFile,
224
+ subject: Declaration,
225
+ node: ast.ClassDef,
226
+ rule: Operation,
227
+ ) -> Iterator[Violation]:
228
+ """Единственная публичная дверь операции."""
229
+ if rule.method is None:
230
+ return
231
+ public = cls._public(node=node)
232
+ names = [method.name for method in public]
233
+ if names == [rule.method]:
234
+ return
235
+ if not public:
236
+ yield cls._says(
237
+ file=file,
238
+ declared=subject,
239
+ message=(
240
+ f"у {subject.name} нет публичного метода; об операции просят "
241
+ f"одним, и он называется {rule.method}()"
242
+ ),
243
+ )
244
+ return
245
+ yield Violation.from_node(
246
+ node=public[0],
247
+ path=file.path,
248
+ code=CODE,
249
+ message=(
250
+ f"{subject.name} предлагает {', '.join(f'{name}()' for name in names)}; "
251
+ f"у операции один публичный метод, и это {rule.method}() — "
252
+ f"остальные приватны или это другой класс"
253
+ ),
254
+ )
255
+
256
+ @classmethod
257
+ def _input(
258
+ cls,
259
+ *,
260
+ file: ParsedFile,
261
+ subject: Declaration,
262
+ node: ast.ClassDef,
263
+ rule: Operation,
264
+ ) -> Iterator[Violation]:
265
+ """Сколько аргументов занимает вход."""
266
+ if rule.max_arguments is None:
267
+ return
268
+ for method in cls._public(node=node):
269
+ count = cls._arguments(node=method)
270
+ if count <= rule.max_arguments:
271
+ continue
272
+ yield Violation.from_node(
273
+ node=method,
274
+ path=file.path,
275
+ code=CODE,
276
+ message=(
277
+ f"{subject.name}.{method.name} — аргументов {count}, предел "
278
+ f"{rule.max_arguments}; передай команду, запрос или DTO"
279
+ ),
280
+ # Пометка снимается с любой строки подписи.
281
+ end_line=max(method.body[0].lineno - 1, method.lineno),
282
+ )
283
+
284
+ @classmethod
285
+ def _arguments(cls, *, node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
286
+ """Всё, что заполняет вызывающий; первый аргумент метода не в счёт.
287
+
288
+ По месту, а не по имени: `self` в `@staticmethod` — обычный аргумент.
289
+ """
290
+ receiver = 0 if cls._static(node=node) else 1
291
+ named = [*node.args.posonlyargs, *node.args.args][receiver:]
292
+ collectors = [one for one in (node.args.vararg, node.args.kwarg) if one is not None]
293
+ return len(named) + len(node.args.kwonlyargs) + len(collectors)
294
+
295
+ @staticmethod
296
+ def _static(*, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
297
+ names = {
298
+ item.id if isinstance(item, ast.Name) else getattr(item, "attr", "")
299
+ for item in node.decorator_list
300
+ }
301
+ return STATIC in names
302
+
303
+ @classmethod
304
+ def _held(
305
+ cls,
306
+ *,
307
+ file: ParsedFile,
308
+ subject: Declaration,
309
+ node: ast.ClassDef,
310
+ rule: Operation,
311
+ ) -> Iterator[Violation]:
312
+ """Тип, которого операция не держит, — в поле или в параметре."""
313
+ for annotation in cls._annotations(node=node):
314
+ held = next(
315
+ (
316
+ name
317
+ for name in cls._typed(node=annotation)
318
+ for mark in rule.forbids
319
+ if mark in name
320
+ ),
321
+ None,
322
+ )
323
+ if held is None:
324
+ continue
325
+ yield Violation.from_node(
326
+ node=annotation,
327
+ path=file.path,
328
+ code=CODE,
329
+ message=(
330
+ f"{subject.name} держит {held}; операции передают то, через что "
331
+ f"она пишет, а транзакция остаётся вызывающему"
332
+ ),
333
+ )
334
+
335
+ @staticmethod
336
+ def _public(*, node: ast.ClassDef) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
337
+ """Методы, до которых может дотянуться вызывающий, в порядке объявления.
338
+
339
+ `@property` считается: это то, что с операции читают, а предложить ей
340
+ нечего, кроме одного.
341
+ """
342
+ return [
343
+ statement
344
+ for statement in node.body
345
+ if isinstance(statement, ast.FunctionDef | ast.AsyncFunctionDef)
346
+ and not statement.name.startswith(PRIVATE)
347
+ ]
348
+
349
+ @staticmethod
350
+ def _annotations(*, node: ast.ClassDef) -> Iterator[ast.expr]:
351
+ """Всё, что класс объявил аннотацией: поля и параметры методов."""
352
+ for child in ast.walk(node):
353
+ match child:
354
+ case ast.AnnAssign(annotation=annotation):
355
+ yield annotation
356
+ case ast.arg(annotation=ast.expr() as annotation):
357
+ yield annotation
358
+ case _:
359
+ continue
360
+
361
+ @staticmethod
362
+ def _typed(*, node: ast.expr) -> Iterator[str]:
363
+ """Имена типов, написанные внутри аннотации."""
364
+ for child in ast.walk(node):
365
+ match child:
366
+ case ast.Name(id=name) | ast.Attribute(attr=name):
367
+ yield name
368
+ # Отложенная ссылка `uow: "PlacementUnitOfWork"` — та же
369
+ # зависимость, записанная ради проверяльщика типов.
370
+ case ast.Constant(value=str() as text):
371
+ yield text
372
+ case _:
373
+ continue
374
+
375
+ @staticmethod
376
+ def _says(
377
+ *,
378
+ file: ParsedFile,
379
+ declared: Declaration,
380
+ message: str,
381
+ ) -> Violation:
382
+ return Violation.from_node(
383
+ node=declared.node,
384
+ path=file.path,
385
+ code=CODE,
386
+ message=message,
387
+ )