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 somewhere in the file
260
- (so the class is live), the field name never appears as an attribute read,
261
- an augmented-assignment target, a class-pattern keyword, or a literal
262
- ``getattr``/``attrgetter`` access anywhere in the file, and the file contains
263
- no non-literal dynamic access, reflective whole-instance consumer
264
- (``asdict``, ``astuple``, ``fields``, ``replace``, ``vars``), ``__dict__``
265
- read, or auto-generated dataclass dunder field read (comparison, set/dict
266
- membership, or whole-instance stringification) that could read it
267
- indirectly. Whole-file analysis runs against ``full_file_content`` when
268
- supplied so an Edit fragment is judged against the reconstructed post-edit
269
- file.
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}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.64.0",
3
+ "version": "1.64.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {