mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-11 07:55:56 +03:00
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:
parent
58094a5c4e
commit
e90e041380
@ -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) {
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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'));
|
||||
|
@ -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",
|
||||
|
15
packages/pyright-internal/src/tests/samples/enum12.py
Normal file
15
packages/pyright-internal/src/tests/samples/enum12.py
Normal 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
|
@ -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']);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user