Added diagnostic check for an enum member with a type annotation. The typing spec says that this should be considered a typing error. This addresses #8060. (#8069)

This commit is contained in:
Eric Traut 2024-06-04 11:11:06 -07:00 committed by GitHub
parent 58094a5c4e
commit e90e041380
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 69 additions and 10 deletions

View File

@ -4971,12 +4971,17 @@ export class Checker extends ParseTreeWalker {
}
ClassType.getSymbolTable(classType).forEach((symbol, name) => {
// Enum members don't have type annotations.
if (symbol.getTypedDeclarations().length > 0) {
return;
}
const symbolType = transformTypeForEnumMember(this._evaluator, classType, name);
// Determine whether this is an enum member. We ignore the presence
// of an annotation in this case because the runtime does. From a
// type checking perspective, if the runtime treats the assignment
// as an enum member but there is a type annotation present, it is
// considered a type checking error.
const symbolType = transformTypeForEnumMember(
this._evaluator,
classType,
name,
/* ignoreAnnotation */ true
);
// Is this symbol a literal instance of the enum class?
if (
@ -4988,6 +4993,19 @@ export class Checker extends ParseTreeWalker {
return;
}
// Enum members should not have type annotations.
const typedDecls = symbol.getTypedDeclarations();
if (typedDecls.length > 0) {
if (typedDecls[0].type === DeclarationType.Variable && typedDecls[0].inferredTypeSource) {
this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.enumMemberTypeAnnotation(),
typedDecls[0].node
);
}
return;
}
// Look for a duplicate assignment.
const decls = symbol.getDeclarations();
if (decls.length >= 2 && decls[0].type === DeclarationType.Variable) {

View File

@ -283,10 +283,19 @@ export function createEnumType(
return classType;
}
// Performs the "magic" that the Enum metaclass does at runtime when it
// transforms a value into an enum instance. If the specified name isn't
// an enum member, this function returns undefined indicating that the
// Enum metaclass does not transform the value.
// By default, if a type annotation is present, the member is not treated
// as a member of the enumeration, but the Enum metaclass ignores such
// annotations. The typing spec indicates that the use of an annotation is
// illegal, so we need to detect this case and report an error.
export function transformTypeForEnumMember(
evaluator: TypeEvaluator,
classType: ClassType,
memberName: string,
ignoreAnnotation = false,
recursionCount = 0
): Type | undefined {
if (recursionCount > maxTypeRecursionCount) {
@ -335,16 +344,24 @@ export function transformTypeForEnumMember(
isMemberOfEnumeration = true;
isUnpackedTuple = true;
valueTypeExprNode = nameNode.parent.parent.rightExpression;
} else if (
nameNode.parent?.nodeType === ParseNodeType.TypeAnnotation &&
nameNode.parent.valueExpression === nameNode
) {
if (ignoreAnnotation) {
isMemberOfEnumeration = true;
}
declaredTypeNode = nameNode.parent.typeAnnotation;
}
// The spec specifically excludes names that start and end with a single underscore.
// This also includes dunder names.
if (isSingleDunderName(nameNode.value)) {
if (isSingleDunderName(memberName)) {
return undefined;
}
// Specifically exclude "value" and "name". These are reserved by the enum metaclass.
if (nameNode.value === 'name' || nameNode.value === 'value') {
if (memberName === 'name' || memberName === 'value') {
return undefined;
}
@ -362,6 +379,7 @@ export function transformTypeForEnumMember(
evaluator,
classType,
valueTypeExprNode.value,
/* ignoreAnnotation */ false,
recursionCount
);
@ -402,7 +420,7 @@ export function transformTypeForEnumMember(
}
// The spec excludes private (mangled) names.
if (isPrivateName(nameNode.value)) {
if (isPrivateName(memberName)) {
return undefined;
}
@ -457,7 +475,7 @@ export function transformTypeForEnumMember(
const enumLiteral = new EnumLiteral(
memberInfo.classType.details.fullName,
memberInfo.classType.details.name,
nameNode.value,
memberName,
valueType
);

View File

@ -436,6 +436,7 @@ export namespace Localizer {
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.enumMemberDelete'));
export const enumMemberSet = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.enumMemberSet'));
export const enumMemberTypeAnnotation = () => getRawString('Diagnostic.enumMemberTypeAnnotation');
export const exceptionGroupIncompatible = () => getRawString('Diagnostic.exceptionGroupIncompatible');
export const exceptionTypeIncorrect = () =>
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.exceptionTypeIncorrect'));

View File

@ -142,6 +142,7 @@
"enumClassOverride": "Enum class \"{name}\" is final and cannot be subclassed",
"enumMemberDelete": "Enum member \"{name}\" cannot be deleted",
"enumMemberSet": "Enum member \"{name}\" cannot be assigned",
"enumMemberTypeAnnotation": "Type annotations are not allowed for enum members",
"exceptionGroupIncompatible": "Exception group syntax (\"except*\") requires Python 3.11 or newer",
"exceptionTypeIncorrect": "\"{type}\" does not derive from BaseException",
"exceptionTypeNotClass": "\"{type}\" is not a valid exception class",

View File

@ -0,0 +1,15 @@
# This sample tests that any attribute that is treated as a member at
# runtime does not have a type annotation. The typing spec indicates that
# type checkers should flag such conditions as errors.
from enum import Enum
from typing import Callable
class Enum1(Enum):
# This should generate an error.
MEMBER: int = 1
_NON_MEMBER_: int = 3
NON_MEMBER_CALLABLE: Callable[[], int] = lambda: 1

View File

@ -998,6 +998,12 @@ test('Enum11', () => {
TestUtils.validateResults(analysisResults, 8);
});
test('Enum12', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['enum12.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('EnumAuto1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['enumAuto1.py']);