Fix class checker crashes on x.__class__ targets outside plain assignments - #11268
Open
codeAnqiang-ma wants to merge 2 commits into
Open
Fix class checker crashes on x.__class__ targets outside plain assignments#11268codeAnqiang-ma wants to merge 2 commits into
x.__class__ targets outside plain assignments#11268codeAnqiang-ma wants to merge 2 commits into
Conversation
…ments visit_assignattr fires for every AssignAttr, but the two __class__ code paths assumed the target's parent is a plain assignment carrying a literal value: - _check_invalid_class_object read node.parent.value, which does not exist for a for-loop, ``with``, or comprehension target, is None for a bare annotation, and its tuple branch indexed .value.elts, which crashes when the right-hand side is a call, a nested tuple, or a too-short literal. - _check_in_slots read node.parent.value the same way for slotted classes, and _has_same_layout_slots called next(assigned_value.infer()) without handling astroid.InferenceError, so an unresolvable value such as an undefined name aborted the whole module. Skip the checks when there is no single assigned value to inspect, and give an unresolvable value the same answer as any other non-class value, matching the existing behavior for Uninferable. Closes pylint-dev#11267 Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #11268 +/- ##
==========================================
+ Coverage 96.36% 96.40% +0.04%
==========================================
Files 178 178
Lines 19953 20042 +89
==========================================
+ Hits 19228 19322 +94
+ Misses 725 720 -5
🚀 New features to boost your workflow:
|
This comment has been minimized.
This comment has been minimized.
Three shapes from the same crash family were left out: - ``nodes.Starred`` is the one non-assignment parent that does carry a usable value (astroid resolves it through the target itself), so skipping it silently dropped ``invalid-class-object`` and ``assigning-non-slot`` on ``head, *foo.__class__ = ...``, which used to be diagnosed correctly. - A ``nodes.List`` assignment target crashed like the tuple one, and was only made silent; it now goes through the same unpacking logic. - ``ClassDef.slots()`` returns None when a class in the mro has no ``__slots__``, so ``zip_longest(slots, None)`` still aborted the whole module on the plainest shape of all, ``instance.__class__ = SlotlessClass``. CPython rejects that assignment too, so ``assigning-non-slot`` is the right answer. The parent-shape guard is now a single ``_assigned_value`` helper, so the two call sites cannot drift apart.
Pierre-Sassoulas
force-pushed
the
fix/class-checker-assignattr-crash
branch
from
August 15, 2026 13:30
b2d65b3 to
ffba5c1
Compare
This comment has been minimized.
This comment has been minimized.
Comment on lines
+1
to
+9
| Fix a crash in the class checker when ``x.__class__`` is an assignment target | ||
| outside a plain assignment, such as a for-loop, ``with``, or comprehension | ||
| target, a bare annotation, a list target, or a tuple target unpacked from a | ||
| non-literal or too-short right-hand side. This affected both | ||
| ``invalid-class-object`` and ``assigning-non-slot``, which also crashed when the | ||
| assigned value could not be resolved by astroid, or when ``__class__`` was | ||
| assigned a class that does not define ``__slots__``. | ||
|
|
||
| Closes #11267 |
Member
There was a problem hiding this comment.
Suggested change
| Fix a crash in the class checker when ``x.__class__`` is an assignment target | |
| outside a plain assignment, such as a for-loop, ``with``, or comprehension | |
| target, a bare annotation, a list target, or a tuple target unpacked from a | |
| non-literal or too-short right-hand side. This affected both | |
| ``invalid-class-object`` and ``assigning-non-slot``, which also crashed when the | |
| assigned value could not be resolved by astroid, or when ``__class__`` was | |
| assigned a class that does not define ``__slots__``. | |
| Closes #11267 | |
| Fix a crash in :ref:`invalid-class-object` and :ref:`assigning-non-slot` when ``__class__`` is assigned outside a simple assignment (e.g. ``for obj.__class__ in classes:``). | |
| Closes #11267 |
Contributor
|
🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉 This comment was generated for commit ffba5c1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type of Changes
Description
Closes #11267
visit_assignattrfires for everyAssignAttr, but the two__class__code paths assumed the target's parent is a plain assignment carrying a literal value, so legal constructs likefor foo.__class__ in [Foo]:aborted the whole module with a fatalastroid-error(F0002):_check_invalid_class_objectreadnode.parent.value, which does not exist for a for-loop,with, or comprehension target and isNonefor a bare annotation; its tuple branch indexed.value.elts, which crashes when the right-hand side is a call, a nested tuple, or a too-short literal (AttributeError/IndexError)._check_in_slotsreadnode.parent.valuethe same way for slotted classes, and_has_same_layout_slotscallednext(assigned_value.infer())without handlingastroid.InferenceError, so an unresolvable value such as an undefined name also crashed.The fix skips the checks when there is no single assigned value to inspect (the same approach as #11173's non-name loop target fix), and gives an unresolvable value in
_has_same_layout_slotsthe same answer as any other non-class value, matching the existing behavior forUninferable(assigning-non-slotis still emitted — covered by the existingClassReassingingInvalidLayoutClassexpectation and a new test).Existing behavior is preserved: the #7467 tuple-unpacking cases keep their messages, including starred unpacking like
self.__class__, *rest = ...(the bound check only skips genuinely unbalanced unpacking).Test evidence
Both extended functional tests fail before the fix and pass after it:
Full functional suite (
python -m pytest tests/test_functional.py -q -n auto): 874 passed, 29 skipped, 14 failed — the failing set is byte-for-byte identical on currentmainin the same environment (macOS, Python 3.13.3; e.g.wrong_import_order,used_before_assignment_py310/311), i.e. pre-existing and unrelated.mypyand a self-run of pylint on the changed file are clean.This PR was prepared with AI assistance as part of a code correctness review; every crash variant was reproduced locally and the fix and tests were reviewed and run by the author.