Repository navigation
UnusedPrivateField falsely flags private class fields read in parameter defaults, decorators, or class body expressions #232
Description
Summary
UnusedPrivateField flags underscore-private fields that have no proven use in the class.
In src/messpy/analyzer.py, _private_fields collects underscore-private class fields and checks their uses:
def _private_fields(node: ast.ClassDef) -> dict[str, int]:
fields: dict[str, int] = {}
for statement in _class_member_statements(node.body):
for name in _field_names(statement):
fields = _with_private_field(fields, name, statement.lineno)
loads: set[str] = set()
unknown = False
for statement in _class_member_statements(node.body):
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
receiver = _instance_receiver(statement)
for item in statement.body:
fields, loads, unknown = _private_field_uses(item, receiver, fields, loads, unknown)
if unknown:
return {}
return {name: line for name, line in fields.items() if name not in loads}_private_fields only scans statement.body of methods for field uses. It does not scan:
- Method parameter default expressions (
method.args.defaultsandmethod.args.kw_defaults), e.g.def connect(self, timeout=_timeout): .... - Method decorators (
method.decorator_list), e.g.@register(_route). - Non-method class statements (e.g.
_base = 10; limit = _base * 2).
Furthermore, _private_field_access only checks ast.Call, ast.AugAssign, and ast.Attribute. When a class field is read within the class scope or method signature, it is evaluated as a bare ast.Name(..., ctx=Load()) rather than self._field or cls._field. _private_field_access never inspects ast.Name nodes, so bare name loads are never added to loads.
As a result, valid and actively used private class fields are falsely reported as unused.
Reproduction
Case 1: Private field used as a method parameter default
cat > repro_default.py <<'PY'
class Client:
_timeout = 30
def connect(self, timeout=_timeout):
return timeout
PY
messpy repro_default.py text unusedcode --only UnusedPrivateFieldObserved:
repro_default.py:2: UnusedPrivateField [priority 3] Avoid unused private fields such as '_timeout'.
# exit code 2
Case 2: Private field used in a class-body expression
cat > repro_body.py <<'PY'
class Math:
_base = 10
limit = _base * 2
PY
messpy repro_body.py text unusedcode --only UnusedPrivateFieldObserved:
repro_body.py:2: UnusedPrivateField [priority 3] Avoid unused private fields such as '_base'.
# exit code 2
Expected:
Both cases should produce exit code 0 with no findings, as the private fields are proven to be used within the class.
Suspected area
In src/messpy/analyzer.py (lines 3291–3305):
def _private_fields(node: ast.ClassDef) -> dict[str, int]:
fields: dict[str, int] = {}
for statement in _class_member_statements(node.body):
for name in _field_names(statement):
fields = _with_private_field(fields, name, statement.lineno)
loads: set[str] = set()
unknown = False
for statement in _class_member_statements(node.body):
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
receiver = _instance_receiver(statement)
for item in statement.body:
fields, loads, unknown = _private_field_uses(item, receiver, fields, loads, unknown)
if unknown:
return {}
return {name: line for name, line in fields.items() if name not in loads}And _private_field_access (lines 3342–3356):
def _private_field_access(
node: ast.AST,
receiver: str | None,
fields: dict[str, int],
loads: set[str],
unknown: bool,
) -> tuple[dict[str, int], set[str], bool]:
if isinstance(node, ast.Call):
return _private_call_access(node, fields, loads, unknown)
if isinstance(node, ast.AugAssign):
return _private_augassign_access(node, receiver, fields, loads, unknown)
if isinstance(node, ast.Attribute):
return _private_attribute_access(node, receiver, fields, loads, unknown)
return fields, loads, unknown_private_fields should inspect all expressions in the class definition (including parameter defaults, decorators, and non-method class-body assignments) and recognize bare ast.Name loads matching declared private field names.
Agent Brief
Category: bug
Summary: unusedcode: check parameter defaults, decorators, and class body expressions for private field uses
Current behavior:
_private_fields only visits method bodies and only checks attribute loads. Private fields referenced as bare names in parameter defaults, decorators, or class body assignments are falsely flagged by UnusedPrivateField.
Desired behavior:
Private fields referenced anywhere within the class definition (including parameter default values, decorators, and class-level expressions) should be recognized as used.
Acceptance criteria:
[ ] A class field used in a method parameter default (def method(self, timeout=_timeout): ...) does not emit an UnusedPrivateField finding.
[ ] A class field used in a method decorator (@decorator(_value)) does not emit an UnusedPrivateField finding.
[ ] A class field used in another class-level statement (limit = _base * 2) does not emit an UnusedPrivateField finding.
[ ] Actually unused private fields continue to be reported.
[ ] All existing test suites pass and self-analysis remains clean (python scripts/verify_self_analysis.py .venv/bin/messpy).
Activity