claude-dev-env 1.64.0 → 1.64.1
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.
|
@@ -217,6 +217,31 @@ def _constructed_class_names(tree: ast.Module) -> set[str]:
|
|
|
217
217
|
return constructed
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
def _module_level_singleton_class_names(tree: ast.Module) -> set[str]:
|
|
221
|
+
"""Return class names constructed by a module-level assignment.
|
|
222
|
+
|
|
223
|
+
A module-level assignment such as ``CURRENT_OS = CurrentOsConfig()`` binds a
|
|
224
|
+
singleton that importer modules read across files, beyond this file's view.
|
|
225
|
+
The in-file read scan cannot observe those cross-module reads, so a field on
|
|
226
|
+
such a class is never provably dead from this file alone. A construction that
|
|
227
|
+
sits inside a function or class body is not collected, so a dataclass built
|
|
228
|
+
for local use stays in scope for the check.
|
|
229
|
+
"""
|
|
230
|
+
singleton_class_names: set[str] = set()
|
|
231
|
+
for each_statement in tree.body:
|
|
232
|
+
if not isinstance(each_statement, (ast.Assign, ast.AnnAssign)):
|
|
233
|
+
continue
|
|
234
|
+
assigned_value = each_statement.value
|
|
235
|
+
if assigned_value is None:
|
|
236
|
+
continue
|
|
237
|
+
for each_node in ast.walk(assigned_value):
|
|
238
|
+
if isinstance(each_node, ast.Call) and isinstance(
|
|
239
|
+
each_node.func, ast.Name
|
|
240
|
+
):
|
|
241
|
+
singleton_class_names.add(each_node.func.id)
|
|
242
|
+
return singleton_class_names
|
|
243
|
+
|
|
244
|
+
|
|
220
245
|
def _is_whole_instance_stringify_call(node: ast.AST) -> bool:
|
|
221
246
|
"""Return whether a call stringifies a whole instance via ``str``/``repr``/``format``."""
|
|
222
247
|
if not isinstance(node, ast.Call):
|
|
@@ -256,17 +281,18 @@ def check_dead_dataclass_fields(
|
|
|
256
281
|
) -> list[str]:
|
|
257
282
|
"""Flag a @dataclass field that the same file constructs but never reads.
|
|
258
283
|
|
|
259
|
-
A field is dead when its dataclass is instantiated
|
|
260
|
-
(so the class is live)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
284
|
+
A field is dead when its dataclass is instantiated by a call in the file
|
|
285
|
+
(so the class is live) but never bound to a module-level singleton (whose
|
|
286
|
+
fields importer modules read across files, beyond this file's view), the
|
|
287
|
+
field name never appears as an attribute read, an augmented-assignment
|
|
288
|
+
target, a class-pattern keyword, or a literal ``getattr``/``attrgetter``
|
|
289
|
+
access anywhere in the file, and the file contains no non-literal dynamic
|
|
290
|
+
access, reflective whole-instance consumer (``asdict``, ``astuple``,
|
|
291
|
+
``fields``, ``replace``, ``vars``), ``__dict__`` read, or auto-generated
|
|
292
|
+
dataclass dunder field read (comparison, set/dict membership, or
|
|
293
|
+
whole-instance stringification) that could read it indirectly. Whole-file
|
|
294
|
+
analysis runs against ``full_file_content`` when supplied so an Edit
|
|
295
|
+
fragment is judged against the reconstructed post-edit file.
|
|
270
296
|
|
|
271
297
|
Args:
|
|
272
298
|
content: The new content under validation (Edit fragment or whole file).
|
|
@@ -300,12 +326,15 @@ def check_dead_dataclass_fields(
|
|
|
300
326
|
| _exported_names(tree)
|
|
301
327
|
)
|
|
302
328
|
constructed_class_names = _constructed_class_names(tree)
|
|
329
|
+
singleton_class_names = _module_level_singleton_class_names(tree)
|
|
303
330
|
issues: list[str] = []
|
|
304
331
|
for each_node in ast.walk(tree):
|
|
305
332
|
if not isinstance(each_node, ast.ClassDef) or not _is_dataclass(each_node):
|
|
306
333
|
continue
|
|
307
334
|
if each_node.name not in constructed_class_names:
|
|
308
335
|
continue
|
|
336
|
+
if each_node.name in singleton_class_names:
|
|
337
|
+
continue
|
|
309
338
|
for each_field_definition in _dataclass_field_definitions(each_node):
|
|
310
339
|
field_name, field_line = each_field_definition
|
|
311
340
|
if field_name in read_names:
|
|
@@ -465,3 +465,43 @@ def test_should_evaluate_full_file_content_when_supplied() -> None:
|
|
|
465
465
|
assert not any(
|
|
466
466
|
"'number'" in each_issue for each_issue in issues
|
|
467
467
|
), f"Read field 'number' must not be flagged, got: {issues}"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_should_not_flag_field_on_module_level_singleton() -> None:
|
|
471
|
+
source = (
|
|
472
|
+
"from dataclasses import dataclass\n"
|
|
473
|
+
"\n"
|
|
474
|
+
"@dataclass(frozen=True)\n"
|
|
475
|
+
"class AppConfig:\n"
|
|
476
|
+
" timeout: int\n"
|
|
477
|
+
" retries: int\n"
|
|
478
|
+
"\n"
|
|
479
|
+
"SETTINGS = AppConfig(timeout=30, retries=3)\n"
|
|
480
|
+
)
|
|
481
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def test_should_flag_local_dataclass_but_not_module_level_singleton() -> None:
|
|
485
|
+
source = (
|
|
486
|
+
"from dataclasses import dataclass\n"
|
|
487
|
+
"\n"
|
|
488
|
+
"@dataclass(frozen=True)\n"
|
|
489
|
+
"class AppConfig:\n"
|
|
490
|
+
" timeout: int\n"
|
|
491
|
+
"\n"
|
|
492
|
+
"@dataclass\n"
|
|
493
|
+
"class LocalRow:\n"
|
|
494
|
+
" url: str\n"
|
|
495
|
+
"\n"
|
|
496
|
+
"SETTINGS = AppConfig(timeout=30)\n"
|
|
497
|
+
"\n"
|
|
498
|
+
"def build() -> LocalRow:\n"
|
|
499
|
+
" return LocalRow(url='x')\n"
|
|
500
|
+
)
|
|
501
|
+
issues = _check(source, PRODUCTION_FILE_PATH)
|
|
502
|
+
assert any(
|
|
503
|
+
"'url'" in each_issue and "LocalRow" in each_issue for each_issue in issues
|
|
504
|
+
), f"Locally-constructed dead field must still be flagged, got: {issues}"
|
|
505
|
+
assert not any(
|
|
506
|
+
"AppConfig" in each_issue for each_issue in issues
|
|
507
|
+
), f"Module-level singleton fields must not be flagged, got: {issues}"
|