Skip to content

Commit e90e041

Browse files
authored
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)
1 parent 58094a5 commit e90e041

File tree

6 files changed

+69
-10
lines changed

6 files changed

+69
-10
lines changed

packages/pyright-internal/src/analyzer/checker.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4971,12 +4971,17 @@ export class Checker extends ParseTreeWalker {
49714971
}
49724972

49734973
ClassType.getSymbolTable(classType).forEach((symbol, name) => {
4974-
// Enum members don't have type annotations.
4975-
if (symbol.getTypedDeclarations().length > 0) {
4976-
return;
4977-
}
4978-
4979-
const symbolType = transformTypeForEnumMember(this._evaluator, classType, name);
4974+
// Determine whether this is an enum member. We ignore the presence
4975+
// of an annotation in this case because the runtime does. From a
4976+
// type checking perspective, if the runtime treats the assignment
4977+
// as an enum member but there is a type annotation present, it is
4978+
// considered a type checking error.
4979+
const symbolType = transformTypeForEnumMember(
4980+
this._evaluator,
4981+
classType,
4982+
name,
4983+
/* ignoreAnnotation */ true
4984+
);
49804985

49814986
// Is this symbol a literal instance of the enum class?
49824987
if (
@@ -4988,6 +4993,19 @@ export class Checker extends ParseTreeWalker {
49884993
return;
49894994
}
49904995

4996+
// Enum members should not have type annotations.
4997+
const typedDecls = symbol.getTypedDeclarations();
4998+
if (typedDecls.length > 0) {
4999+
if (typedDecls[0].type === DeclarationType.Variable && typedDecls[0].inferredTypeSource) {
5000+
this._evaluator.addDiagnostic(
5001+
DiagnosticRule.reportGeneralTypeIssues,
5002+
LocMessage.enumMemberTypeAnnotation(),
5003+
typedDecls[0].node
5004+
);
5005+
}
5006+
return;
5007+
}
5008+
49915009
// Look for a duplicate assignment.
49925010
const decls = symbol.getDeclarations();
49935011
if (decls.length >= 2 && decls[0].type === DeclarationType.Variable) {

packages/pyright-internal/src/analyzer/enums.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,19 @@ export function createEnumType(
283283
return classType;
284284
}
285285

286+
// Performs the "magic" that the Enum metaclass does at runtime when it
287+
// transforms a value into an enum instance. If the specified name isn't
288+
// an enum member, this function returns undefined indicating that the
289+
// Enum metaclass does not transform the value.
290+
// By default, if a type annotation is present, the member is not treated
291+
// as a member of the enumeration, but the Enum metaclass ignores such
292+
// annotations. The typing spec indicates that the use of an annotation is
293+
// illegal, so we need to detect this case and report an error.
286294
export function transformTypeForEnumMember(
287295
evaluator: TypeEvaluator,
288296
classType: ClassType,
289297
memberName: string,
298+
ignoreAnnotation = false,
290299
recursionCount = 0
291300
): Type | undefined {
292301
if (recursionCount > maxTypeRecursionCount) {
@@ -335,16 +344,24 @@ export function transformTypeForEnumMember(
335344
isMemberOfEnumeration = true;
336345
isUnpackedTuple = true;
337346
valueTypeExprNode = nameNode.parent.parent.rightExpression;
347+
} else if (
348+
nameNode.parent?.nodeType === ParseNodeType.TypeAnnotation &&
349+
nameNode.parent.valueExpression === nameNode
350+
) {
351+
if (ignoreAnnotation) {
352+
isMemberOfEnumeration = true;
353+
}
354+
declaredTypeNode = nameNode.parent.typeAnnotation;
338355
}
339356

340357
// The spec specifically excludes names that start and end with a single underscore.
341358
// This also includes dunder names.
342-
if (isSingleDunderName(nameNode.value)) {
359+
if (isSingleDunderName(memberName)) {
343360
return undefined;
344361
}
345362

346363
// Specifically exclude "value" and "name". These are reserved by the enum metaclass.
347-
if (nameNode.value === 'name' || nameNode.value === 'value') {
364+
if (memberName === 'name' || memberName === 'value') {
348365
return undefined;
349366
}
350367

@@ -362,6 +379,7 @@ export function transformTypeForEnumMember(
362379
evaluator,
363380
classType,
364381
valueTypeExprNode.value,
382+
/* ignoreAnnotation */ false,
365383
recursionCount
366384
);
367385

@@ -402,7 +420,7 @@ export function transformTypeForEnumMember(
402420
}
403421

404422
// The spec excludes private (mangled) names.
405-
if (isPrivateName(nameNode.value)) {
423+
if (isPrivateName(memberName)) {
406424
return undefined;
407425
}
408426

@@ -457,7 +475,7 @@ export function transformTypeForEnumMember(
457475
const enumLiteral = new EnumLiteral(
458476
memberInfo.classType.details.fullName,
459477
memberInfo.classType.details.name,
460-
nameNode.value,
478+
memberName,
461479
valueType
462480
);
463481

packages/pyright-internal/src/localization/localize.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ export namespace Localizer {
436436
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.enumMemberDelete'));
437437
export const enumMemberSet = () =>
438438
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.enumMemberSet'));
439+
export const enumMemberTypeAnnotation = () => getRawString('Diagnostic.enumMemberTypeAnnotation');
439440
export const exceptionGroupIncompatible = () => getRawString('Diagnostic.exceptionGroupIncompatible');
440441
export const exceptionTypeIncorrect = () =>
441442
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.exceptionTypeIncorrect'));

packages/pyright-internal/src/localization/package.nls.en-us.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"enumClassOverride": "Enum class \"{name}\" is final and cannot be subclassed",
143143
"enumMemberDelete": "Enum member \"{name}\" cannot be deleted",
144144
"enumMemberSet": "Enum member \"{name}\" cannot be assigned",
145+
"enumMemberTypeAnnotation": "Type annotations are not allowed for enum members",
145146
"exceptionGroupIncompatible": "Exception group syntax (\"except*\") requires Python 3.11 or newer",
146147
"exceptionTypeIncorrect": "\"{type}\" does not derive from BaseException",
147148
"exceptionTypeNotClass": "\"{type}\" is not a valid exception class",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This sample tests that any attribute that is treated as a member at
2+
# runtime does not have a type annotation. The typing spec indicates that
3+
# type checkers should flag such conditions as errors.
4+
5+
from enum import Enum
6+
from typing import Callable
7+
8+
9+
class Enum1(Enum):
10+
# This should generate an error.
11+
MEMBER: int = 1
12+
13+
_NON_MEMBER_: int = 3
14+
15+
NON_MEMBER_CALLABLE: Callable[[], int] = lambda: 1

packages/pyright-internal/src/tests/typeEvaluator3.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,12 @@ test('Enum11', () => {
998998
TestUtils.validateResults(analysisResults, 8);
999999
});
10001000

1001+
test('Enum12', () => {
1002+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['enum12.py']);
1003+
1004+
TestUtils.validateResults(analysisResults, 1);
1005+
});
1006+
10011007
test('EnumAuto1', () => {
10021008
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['enumAuto1.py']);
10031009

0 commit comments

Comments
 (0)