From 7afbbcaadf3cb6d4c0b9d9139361f93e5ade7196 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 15 Feb 2020 10:22:52 -0700 Subject: [PATCH] Added error reporting for instance and class variables within methods declared in a Protocol class. PEP 544 indicates that these should be flagged as an error. --- server/src/analyzer/typeEvaluator.ts | 14 ++++++++++++++ server/src/analyzer/types.ts | 9 ++++++++- server/src/tests/checker.test.ts | 6 ++++++ server/src/tests/samples/protocol4.py | 16 ++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 server/src/tests/samples/protocol4.py diff --git a/server/src/analyzer/typeEvaluator.ts b/server/src/analyzer/typeEvaluator.ts index 6e02070f07a6..cad1446ce438 100644 --- a/server/src/analyzer/typeEvaluator.ts +++ b/server/src/analyzer/typeEvaluator.ts @@ -1631,6 +1631,14 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator { assignTypeToMemberVariable(target, type, false, srcExpr); } } + + // Assignments to instance or class variables through "self" or "cls" is not + // allowed for protocol classes. + if (ClassType.isProtocolClass(classTypeResults.classType)) { + addError( + `Assignment to instance or class variables not allowed within a Protocol class`, + target.memberName); + } } } } @@ -5866,6 +5874,12 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator { } } + if (fileInfo.executionEnvironment.pythonVersion >= PythonVersion.V38) { + if (ClassType.isBuiltIn(argType, 'Protocol')) { + classType.details.flags |= ClassTypeFlags.ProtocolClass; + } + } + // If the class directly derives from TypedDict or from a class that is // a TypedDict, it is considered a TypedDict. if (ClassType.isBuiltIn(argType, 'TypedDict') || ClassType.isTypedDictClass(argType)) { diff --git a/server/src/analyzer/types.ts b/server/src/analyzer/types.ts index ee93ebf01ee2..405fd378042b 100644 --- a/server/src/analyzer/types.ts +++ b/server/src/analyzer/types.ts @@ -184,7 +184,10 @@ export const enum ClassTypeFlags { // The class is decorated with a "@final" decorator // indicating that it cannot be subclassed. - Final = 1 << 10 + Final = 1 << 10, + + // The class derives directly from "Protocol". + ProtocolClass = 1 << 11 } interface ClassDetails { @@ -328,6 +331,10 @@ export namespace ClassType { return !!(classType.details.flags & ClassTypeFlags.Final); } + export function isProtocolClass(classType: ClassType) { + return !!(classType.details.flags & ClassTypeFlags.ProtocolClass); + } + export function getDataClassParameters(classType: ClassType): FunctionParameter[] { return classType.details.dataClassParameters || []; } diff --git a/server/src/tests/checker.test.ts b/server/src/tests/checker.test.ts index 9e52c3cc679c..5f72ce75f786 100644 --- a/server/src/tests/checker.test.ts +++ b/server/src/tests/checker.test.ts @@ -894,6 +894,12 @@ test('Protocol3', () => { validateResults(analysisResults, 1); }); +test('Protocol4', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol4.py']); + + validateResults(analysisResults, 2); +}); + test('TypedDict1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDict1.py']); diff --git a/server/src/tests/samples/protocol4.py b/server/src/tests/samples/protocol4.py new file mode 100644 index 000000000000..a2d8a4663164 --- /dev/null +++ b/server/src/tests/samples/protocol4.py @@ -0,0 +1,16 @@ +# This sample tests that instance and class variables +# assigned within a Protocol method are flagged as errors. + +from typing import List, Protocol + +class Template(Protocol): + def method(self) -> None: + # This should be an error + self.temp: List[int] = [] + + @classmethod + def cls_method(cls) -> None: + # This should be an error + cls.test2 = 3 + +