Skip to content

Reject @runtime_checkable on non-protocol classes - #11646

Open
Henry Su (hsusul) wants to merge 2 commits into
microsoft:mainfrom
hsusul:fix/runtime-checkable-requires-protocol
Open

Reject @runtime_checkable on non-protocol classes#11646
Henry Su (hsusul) wants to merge 2 commits into
microsoft:mainfrom
hsusul:fix/runtime-checkable-requires-protocol

Conversation

@hsusul

Copy link
Copy Markdown
Contributor

Summary

Test plan

  • npx jest typeEvaluator7.test.ts -t 'Protocol54|Protocol7' --forceExit (from packages/pyright-internal)
  • Runtime: @runtime_checkable class Child(ParentProtocol) raises TypeError; class Child(ParentProtocol, Protocol) succeeds
  • Confirm the decorator diagnostic appears on a non-protocol class in the language server

CPython requires Protocol in the base list; a subclass of a protocol is
a concrete class unless it also lists Protocol, and the decorator raises TypeError.
@rchiodo

Rich Chiodo (rchiodo) commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR.

DiagnosticRule.reportGeneralTypeIssues,
LocMessage.runtimeCheckableNotProtocol(),
decoratorNode.d.expr
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Class decorators are applied bottom-up, but this validates originalClassType rather than the type produced by decorators below @runtime_checkable. A class-replacing decorator could therefore allow a source Protocol even though runtime_checkable receives a non-protocol class at runtime. Validate the currently decorated class and add a composition regression test.

const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol54.py']);

TestUtils.validateResults(analysisResults, 2);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info · Optional note

The aggregate diagnostic count can pass if diagnostics move from P2 and C1 to either valid control. Assert diagnostic locations or identities so the test proves exactly the two invalid declarations are rejected.

DiagnosticRule.reportGeneralTypeIssues,
LocMessage.runtimeCheckableNotProtocol(),
decoratorNode.d.expr
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

When vendoring this change into pyrx, mirror it in packages/pylance-internal/src/analyzer/decorators.ts, which still sets RuntimeCheckable unconditionally, and add async-mode regression coverage. Otherwise Pylance's async evaluator can retain the old behavior.

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo Rich Chiodo (rchiodo) added the review-auto:approved Automated review: no blocking findings (approval posted). label Aug 17, 2026
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol54.py']);

TestUtils.validateResults(analysisResults, 2);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info · Optional note

This checks only the aggregate diagnostic count, so unrelated diagnostics could satisfy the test. Assert the diagnostic message and decorator ranges for both invalid classes if the harness supports it.

[verified]

@rchiodo Rich Chiodo (rchiodo) added review-auto:changes-requested Automated review: posted blocking findings to address. and removed review-auto:approved Automated review: no blocking findings (approval posted). labels Aug 17, 2026
Class decorators are applied bottom-up, so check the class type that
runtime_checkable actually receives rather than the original class, and
assert the reported diagnostic locations in the sample test.
} else {
originalClassType.shared.flags |= ClassTypeFlags.RuntimeCheckable;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

When a lower decorator returns a non-runtime-checkable protocol, this validates the replacement type but sets RuntimeCheckable on the discarded original class. Set the flag on the validated decorated class and add coverage using a replacement protocol that is not already runtime-checkable.

@github-actions

Copy link
Copy Markdown
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-auto:changes-requested Automated review: posted blocking findings to address.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@runtime_checkable should require a direct inheritance from Protocol

2 participants