Skip to content

UnusedPrivateField falsely flags private class fields read in parameter defaults, decorators, or class body expressions #232

Closed

Description

@jonbaldie

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:

  1. Method parameter default expressions (method.args.defaults and method.args.kw_defaults), e.g. def connect(self, timeout=_timeout): ....
  2. Method decorators (method.decorator_list), e.g. @register(_route).
  3. 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 UnusedPrivateField

Observed:

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 UnusedPrivateField

Observed:

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

  1. added
    bugSomething isn't working
    ready-for-agentFully specified, ready for an AFK agent
    on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions