From 53e8cd4145e46697444402d9acb175850141b4f0 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 13 Jan 2023 21:16:07 -0800 Subject: [PATCH] Added provisional support for draft PEP 702 (marking deprecations using the type system). --- README.md | 1 + docs/configuration.md | 3 + .../pyright-internal/src/analyzer/checker.ts | 129 ++++++++++++++---- .../src/analyzer/typeEvaluator.ts | 106 ++++++++++++-- .../src/analyzer/typeEvaluatorTypes.ts | 8 ++ .../pyright-internal/src/analyzer/types.ts | 2 + .../src/common/configOptions.ts | 7 + .../src/common/diagnosticRules.ts | 1 + .../src/localization/localize.ts | 2 + .../src/localization/package.nls.en-us.json | 2 + .../src/tests/checker.test.ts | 22 +++ .../src/tests/samples/deprecated2.py | 71 ++++++++++ .../src/tests/samples/deprecated3.py | 25 ++++ .../stdlib/typing_extensions.pyi | 6 + packages/vscode-pyright/README.md | 1 + packages/vscode-pyright/package.json | 11 ++ .../schemas/pyrightconfig.schema.json | 6 + 17 files changed, 366 insertions(+), 37 deletions(-) create mode 100644 packages/pyright-internal/src/tests/samples/deprecated2.py create mode 100644 packages/pyright-internal/src/tests/samples/deprecated3.py diff --git a/README.md b/README.md index 381b20eb0..bca45c7a5 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Pyright supports [configuration files](/docs/configuration.md) that provide gran * [PEP 695](https://www.python.org/dev/peps/pep-0695/) (draft) Type parameter syntax * [PEP 696](https://www.python.org/dev/peps/pep-0696/) (draft) Type defaults for TypeVarLikes * [PEP 698](https://www.python.org/dev/peps/pep-0698/) (draft) Override decorator for static typing +* [PEP 702](https://www.python.org/dev/peps/pep-0702/) (draft) Marking deprecations * Type inference for function return values, instance variables, class variables, and globals * Type guards that understand conditional code flow constructs like if/else statements diff --git a/docs/configuration.md b/docs/configuration.md index f53608f0e..75ae6dfe1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -112,6 +112,8 @@ The following settings control pyright’s diagnostic output (warnings or errors **reportConstantRedefinition** [boolean or string, optional]: Generate or suppress diagnostics for attempts to redefine variables whose names are all-caps with underscores and numerals. The default value for this setting is 'none'. + **reportDeprecated** [boolean or string, optional]: Generate or suppress diagnostics for use of a class or function that has been marked as deprecated. The default value for this setting is 'none'. + **reportIncompatibleMethodOverride** [boolean or string, optional]: Generate or suppress diagnostics for methods that override a method of the same name in a base class in an incompatible manner (wrong number of parameters, incompatible parameter types, or incompatible return type). The default value for this setting is 'none'. **reportIncompatibleVariableOverride** [boolean or string, optional]: Generate or suppress diagnostics for class variable declarations that override a symbol of the same name in a base class with a type that is incompatible with the base class symbol type. The default value for this setting is 'none'. @@ -321,6 +323,7 @@ The following table lists the default severity levels for each diagnostic rule w | reportUnboundVariable | "none" | "error" | "error" | | reportUnusedCoroutine | "none" | "error" | "error" | | reportConstantRedefinition | "none" | "none" | "error" | +| reportDeprecated | "none" | "none" | "error" | | reportDuplicateImport | "none" | "none" | "error" | | reportFunctionMemberAccess | "none" | "none" | "error" | | reportImportCycles | "none" | "none" | "error" | diff --git a/packages/pyright-internal/src/analyzer/checker.ts b/packages/pyright-internal/src/analyzer/checker.ts index 1e94bf1a2..2b12a0809 100644 --- a/packages/pyright-internal/src/analyzer/checker.ts +++ b/packages/pyright-internal/src/analyzer/checker.ts @@ -1374,9 +1374,9 @@ export class Checker extends ParseTreeWalker { this._reportUnboundName(node); } - // Report the use of a deprecated symbol. For now, this functionality - // is disabled. We'll leave it in place for the future. - // this._reportDeprecatedUse(node); + // Report the use of a deprecated symbol. + const type = this._evaluator.getType(node); + this._reportDeprecatedUse(node, type); return true; } @@ -1392,7 +1392,9 @@ export class Checker extends ParseTreeWalker { } override visitMemberAccess(node: MemberAccessNode) { - this._evaluator.getType(node); + const type = this._evaluator.getType(node); + this._reportDeprecatedUse(node.memberName, type); + this._conditionallyReportPrivateUsage(node.memberName); // Walk the leftExpression but not the memberName. @@ -1475,6 +1477,9 @@ export class Checker extends ParseTreeWalker { break; } + const type = this._evaluator.getType(node.alias ?? node.name); + this._reportDeprecatedUse(node.name, type); + return false; } @@ -3562,31 +3567,109 @@ export class Checker extends ParseTreeWalker { return false; } - private _reportDeprecatedUse(node: NameNode) { - const deprecatedForm = deprecatedAliases.get(node.value) ?? deprecatedSpecialForms.get(node.value); - - if (!deprecatedForm) { - return; - } - - const type = this._evaluator.getType(node); - + private _reportDeprecatedUse(node: NameNode, type: Type | undefined) { if (!type) { return; } - if (!isInstantiableClass(type) || type.details.fullName !== deprecatedForm.fullName) { - return; + let errorMessage: string | undefined; + let deprecatedMessage: string | undefined; + + doForEachSubtype(type, (subtype) => { + if (isClass(subtype)) { + if ( + !subtype.includeSubclasses && + subtype.details.deprecatedMessage !== undefined && + node.value === subtype.details.name + ) { + deprecatedMessage = subtype.details.deprecatedMessage; + errorMessage = Localizer.Diagnostic.deprecatedClass(); + } + } else if (isFunction(subtype)) { + if (subtype.details.deprecatedMessage !== undefined && node.value === subtype.details.name) { + deprecatedMessage = subtype.details.deprecatedMessage; + errorMessage = Localizer.Diagnostic.deprecatedFunction(); + } + } else if (isOverloadedFunction(subtype)) { + // Determine if the node is part of a call expression. If so, + // we can determine which overload(s) were used to satisfy + // the call expression and determine whether any of them + // are deprecated. + let callTypeResult: TypeResult | undefined; + if (node.parent?.nodeType === ParseNodeType.Call && node.parent.leftExpression === node) { + callTypeResult = this._evaluator.getTypeResult(node.parent); + } else if ( + node.parent?.nodeType === ParseNodeType.MemberAccess && + node.parent.memberName === node && + node.parent.parent?.nodeType === ParseNodeType.Call && + node.parent.parent.leftExpression === node.parent + ) { + callTypeResult = this._evaluator.getTypeResult(node.parent.parent); + } + + if ( + callTypeResult && + callTypeResult.overloadsUsedForCall && + callTypeResult.overloadsUsedForCall.length > 0 + ) { + callTypeResult.overloadsUsedForCall.forEach((overload) => { + if (overload.details.deprecatedMessage !== undefined && node.value === overload.details.name) { + deprecatedMessage = overload.details.deprecatedMessage; + errorMessage = Localizer.Diagnostic.deprecatedFunction(); + } + }); + } + } + }); + + if (errorMessage) { + const diag = new DiagnosticAddendum(); + if (deprecatedMessage) { + diag.addMessage(deprecatedMessage); + } + + if (this._fileInfo.diagnosticRuleSet.reportDeprecated === 'none') { + this._evaluator.addDeprecated(errorMessage + diag.getString(), node); + } else { + this._evaluator.addDiagnostic( + this._fileInfo.diagnosticRuleSet.reportDeprecated, + DiagnosticRule.reportDeprecated, + errorMessage + diag.getString(), + node + ); + } } - if (this._fileInfo.executionEnvironment.pythonVersion >= deprecatedForm.version) { - this._evaluator.addDeprecated( - Localizer.Diagnostic.deprecatedType().format({ - version: versionToString(deprecatedForm.version), - replacement: deprecatedForm.replacementText, - }), - node - ); + // We'll leave this disabled for now because this would be too noisy for most + // code bases. We may want to add it at some future date. + if (0) { + const deprecatedForm = deprecatedAliases.get(node.value) ?? deprecatedSpecialForms.get(node.value); + + if (deprecatedForm) { + if (isInstantiableClass(type) && type.details.fullName === deprecatedForm.fullName) { + if (this._fileInfo.executionEnvironment.pythonVersion >= deprecatedForm.version) { + if (this._fileInfo.diagnosticRuleSet.reportDeprecated === 'none') { + this._evaluator.addDeprecated( + Localizer.Diagnostic.deprecatedType().format({ + version: versionToString(deprecatedForm.version), + replacement: deprecatedForm.replacementText, + }), + node + ); + } else { + this._evaluator.addDiagnostic( + this._fileInfo.diagnosticRuleSet.reportDeprecated, + DiagnosticRule.reportDeprecated, + Localizer.Diagnostic.deprecatedType().format({ + version: versionToString(deprecatedForm.version), + replacement: deprecatedForm.replacementText, + }), + node + ); + } + } + } + } } } diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 0816b0226..655c40315 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -7287,6 +7287,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions if (callResult.argumentErrors) { typeResult.typeErrors = true; + } else { + typeResult.overloadsUsedForCall = callResult.overloadsUsedForCall; } if (callResult.isTypeIncomplete) { @@ -7698,6 +7700,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions typeVarContext: TypeVarContext; }[] = []; let isTypeIncomplete = false; + const overloadsUsedForCall: FunctionType[] = []; for (let expandedTypesIndex = 0; expandedTypesIndex < expandedArgTypes.length; expandedTypesIndex++) { let matchedOverload: FunctionType | undefined; @@ -7751,6 +7754,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions } if (!callResult.argumentErrors && callResult.returnType) { + overloadsUsedForCall.push(overload); + matchedOverload = overload; matchedOverloads.push({ overload: matchedOverload, @@ -7804,7 +7809,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions } if (!matchedOverload) { - return { argumentErrors: true, isTypeIncomplete }; + return { argumentErrors: true, isTypeIncomplete, overloadsUsedForCall }; } } @@ -7839,6 +7844,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions returnType: combineTypes(returnTypes), isTypeIncomplete, specializedInitSelfType: finalCallResult.specializedInitSelfType, + overloadsUsedForCall, }; } @@ -7952,7 +7958,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions ); } - return { argumentErrors: true, isTypeIncomplete: false }; + return { argumentErrors: true, isTypeIncomplete: false, overloadsUsedForCall: [] }; } // Create a helper lambda that evaluates the overload that matches @@ -8049,7 +8055,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions return { ...result, argumentErrors: true }; } - return { argumentErrors: true, isTypeIncomplete: false }; + return { argumentErrors: true, isTypeIncomplete: false, overloadsUsedForCall: [] }; } // Replaces each item in the expandedArgTypes with n items where n is @@ -8124,6 +8130,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions let reportedErrors = false; let isTypeIncomplete = false; let usedMetaclassCallMethod = false; + const overloadsUsedForCall: FunctionType[] = []; // Create a helper function that determines whether we should skip argument // validation for either __init__ or __new__. This is required for certain @@ -8166,6 +8173,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions if (expectedCallResult.isTypeIncomplete) { isTypeIncomplete = true; } + + overloadsUsedForCall.push(...expectedCallResult.overloadsUsedForCall); } } @@ -8202,6 +8211,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions if (callResult.isTypeIncomplete) { isTypeIncomplete = true; } + + overloadsUsedForCall.push(...callResult.overloadsUsedForCall); } else { reportedErrors = true; } @@ -8425,7 +8436,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions } } - const result: CallResult = { argumentErrors: reportedErrors, returnType, isTypeIncomplete }; + const result: CallResult = { + argumentErrors: reportedErrors, + returnType, + isTypeIncomplete, + overloadsUsedForCall, + }; return result; } @@ -8443,6 +8459,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions ): CallResult | undefined { let isTypeIncomplete = false; let argumentErrors = false; + const overloadsUsedForCall: FunctionType[] = []; const returnType = mapSubtypes(expectedType, (expectedSubType) => { expectedSubType = transformPossibleRecursiveTypeAlias(expectedSubType); @@ -8486,6 +8503,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions argumentErrors = true; } + overloadsUsedForCall.push(...callResult.overloadsUsedForCall); + return applyExpectedSubtypeForConstructor(type, expectedSubType, typeVarContext); } } @@ -8497,7 +8516,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions return undefined; } - return { returnType, isTypeIncomplete, argumentErrors }; + return { returnType, isTypeIncomplete, argumentErrors, overloadsUsedForCall }; } function applyExpectedSubtypeForConstructor( @@ -8584,9 +8603,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions let argumentErrors = false; let isTypeIncomplete = false; let specializedInitSelfType: Type | undefined; + const overloadsUsedForCall: FunctionType[] = []; if (recursionCount > maxTypeRecursionCount) { - return { returnType: UnknownType.create(), argumentErrors: true }; + return { returnType: UnknownType.create(), argumentErrors: true, overloadsUsedForCall }; } recursionCount++; @@ -8601,7 +8621,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions }), exprNode ); - return { returnType: UnknownType.create(), argumentErrors: true }; + return { returnType: UnknownType.create(), argumentErrors: true, overloadsUsedForCall }; } const returnType = mapSubtypesExpandTypeVars( @@ -8675,6 +8695,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions isTypeIncomplete = true; } + overloadsUsedForCall.push(...functionResult.overloadsUsedForCall); + if (functionResult.argumentErrors) { argumentErrors = true; } else { @@ -8747,6 +8769,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions expectedType ); + overloadsUsedForCall.push(...functionResult.overloadsUsedForCall); + if (functionResult.isTypeIncomplete) { isTypeIncomplete = true; } @@ -8953,6 +8977,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions expectedType ); + overloadsUsedForCall.push(...constructorResult.overloadsUsedForCall); + if (constructorResult.argumentErrors) { argumentErrors = true; } @@ -9023,6 +9049,9 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions expectedType, recursionCount ); + + overloadsUsedForCall.push(...functionResult.overloadsUsedForCall); + if (functionResult.argumentErrors) { argumentErrors = true; } @@ -9082,6 +9111,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions recursionCount ); + overloadsUsedForCall.push(...callResult.overloadsUsedForCall); + if (callResult.argumentErrors) { argumentErrors = true; } @@ -9109,6 +9140,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions returnType: isNever(returnType) && !returnType.isNoReturn ? undefined : returnType, isTypeIncomplete, specializedInitSelfType, + overloadsUsedForCall, }; } @@ -10507,6 +10539,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions isTypeIncomplete, activeParam: matchResults.activeParam, specializedInitSelfType, + overloadsUsedForCall: argumentErrors ? [] : [type], }; } @@ -10551,6 +10584,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions return { argumentErrors: true, activeParam: matchResults.activeParam, + overloadsUsedForCall: [], }; } @@ -16176,6 +16210,16 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions ); } } + + if (isOverloadedFunction(decoratorCallType)) { + if ( + decoratorCallType.overloads.length > 0 && + decoratorCallType.overloads[0].details.builtInName === 'deprecated' + ) { + originalClassType.details.deprecatedMessage = getCustomDeprecationMessage(decoratorNode); + return inputClassType; + } + } } if (isOverloadedFunction(decoratorType)) { @@ -16190,6 +16234,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions ); return inputClassType; } + + if (decoratorType.overloads.length > 0 && decoratorType.overloads[0].details.builtInName === 'deprecated') { + originalClassType.details.deprecatedMessage = getCustomDeprecationMessage(decoratorNode); + return inputClassType; + } } else if (isFunction(decoratorType)) { if (decoratorType.details.builtInName === 'final') { originalClassType.details.flags |= ClassTypeFlags.Final; @@ -16198,7 +16247,9 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions // behavior because its function definition results in a cyclical // dependency between builtins, typing and _typeshed stubs. return inputClassType; - } else if (decoratorType.details.builtInName === 'runtime_checkable') { + } + + if (decoratorType.details.builtInName === 'runtime_checkable') { originalClassType.details.flags |= ClassTypeFlags.RuntimeCheckable; // Don't call getTypeOfDecorator for runtime_checkable. It appears @@ -16238,6 +16289,22 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions return getTypeOfDecorator(decoratorNode, inputClassType); } + // Given a @typing.deprecated decorator node, returns either '' or a custom + // deprecation message if one is provided. + function getCustomDeprecationMessage(decorator: DecoratorNode): string { + if ( + decorator.expression.nodeType === ParseNodeType.Call && + decorator.expression.arguments.length > 0 && + decorator.expression.arguments[0].argumentCategory === ArgumentCategory.Simple && + decorator.expression.arguments[0].valueExpression.nodeType === ParseNodeType.StringList && + decorator.expression.arguments[0].valueExpression.strings.length === 1 + ) { + return decorator.expression.arguments[0].valueExpression.strings[0].value; + } + + return ''; + } + // Runs any registered "callback hooks" that depend on the specified class type. // This allows us to complete any work that requires dependent classes to be // completed. @@ -17203,6 +17270,16 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions return inputFunctionType; } } + + if (isOverloadedFunction(decoratorCallType)) { + if ( + decoratorCallType.overloads.length > 0 && + decoratorCallType.overloads[0].details.builtInName === 'deprecated' + ) { + undecoratedType.details.deprecatedMessage = getCustomDeprecationMessage(decoratorNode); + return inputFunctionType; + } + } } let returnType = getTypeOfDecorator(decoratorNode, inputFunctionType); @@ -17249,6 +17326,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions } } } + } else if (isOverloadedFunction(decoratorType)) { + if (decoratorType.overloads.length > 0 && decoratorType.overloads[0].details.builtInName === 'deprecated') { + undecoratedType.details.deprecatedMessage = getCustomDeprecationMessage(decoratorNode); + return inputFunctionType; + } } else if (isInstantiableClass(decoratorType)) { if (ClassType.isBuiltIn(decoratorType)) { switch (decoratorType.details.name) { @@ -18695,17 +18777,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions // don't bother doing additional work. let cacheEntry = readTypeCacheEntry(subnode); if (cacheEntry && !cacheEntry.typeResult.isIncomplete) { - return { type: cacheEntry.typeResult.type }; + return cacheEntry.typeResult; } callback(); cacheEntry = readTypeCacheEntry(subnode); if (cacheEntry) { - return { - type: cacheEntry.typeResult.type, - isIncomplete: cacheEntry.typeResult.isIncomplete, - expectedTypeDiagAddendum: cacheEntry.typeResult.expectedTypeDiagAddendum, - }; + return cacheEntry.typeResult; } return undefined; diff --git a/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts b/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts index d5271ab8b..74ed76a87 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts @@ -182,6 +182,9 @@ export interface TypeResult { // Is the type wrapped in a "Required" or "NotRequired" class? isRequired?: boolean; isNotRequired?: boolean; + + // If a call expression, which overloads were used to satisfy it? + overloadsUsedForCall?: FunctionType[]; } export interface TypeResultWithNode extends TypeResult { @@ -319,6 +322,11 @@ export interface CallResult { // is used for overloaded constructors where the arguments to the // constructor influence the specialized type of the constructed object. specializedInitSelfType?: Type | undefined; + + // The overload or overloads used to satisfy the call. There can + // be multiple overloads in the case where the call type is a union + // or we have used union expansion for arguments. + overloadsUsedForCall: FunctionType[]; } export interface PrintTypeOptions { diff --git a/packages/pyright-internal/src/analyzer/types.ts b/packages/pyright-internal/src/analyzer/types.ts index 3e7fe5f35..5aa9d7e28 100644 --- a/packages/pyright-internal/src/analyzer/types.ts +++ b/packages/pyright-internal/src/analyzer/types.ts @@ -497,6 +497,7 @@ interface ClassDetails { typeParameters: TypeVarType[]; typeVarScopeId?: TypeVarScopeId | undefined; docString?: string | undefined; + deprecatedMessage?: string | undefined; dataClassEntries?: DataClassEntry[] | undefined; dataClassBehaviors?: DataClassBehaviors | undefined; typedDictEntries?: Map | undefined; @@ -1212,6 +1213,7 @@ interface FunctionDetails { constructorTypeVarScopeId?: TypeVarScopeId | undefined; builtInName?: string | undefined; docString?: string | undefined; + deprecatedMessage?: string | undefined; // Transforms to apply if this function is used // as a decorator. diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index eec6bd8fb..2a44a9c44 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -187,6 +187,9 @@ export interface DiagnosticRuleSet { // Report attempts to redefine variables that are in all-caps. reportConstantRedefinition: DiagnosticLevel; + // Report use of deprecated classes or functions. + reportDeprecated: DiagnosticLevel; + // Report usage of method override that is incompatible with // the base class method of the same name? reportIncompatibleMethodOverride: DiagnosticLevel; @@ -361,6 +364,7 @@ export function getDiagLevelDiagnosticRules() { DiagnosticRule.reportTypeCommentUsage, DiagnosticRule.reportPrivateImportUsage, DiagnosticRule.reportConstantRedefinition, + DiagnosticRule.reportDeprecated, DiagnosticRule.reportIncompatibleMethodOverride, DiagnosticRule.reportIncompatibleVariableOverride, DiagnosticRule.reportInconsistentConstructor, @@ -445,6 +449,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet { reportTypeCommentUsage: 'none', reportPrivateImportUsage: 'none', reportConstantRedefinition: 'none', + reportDeprecated: 'none', reportIncompatibleMethodOverride: 'none', reportIncompatibleVariableOverride: 'none', reportInconsistentConstructor: 'none', @@ -525,6 +530,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet { reportTypeCommentUsage: 'none', reportPrivateImportUsage: 'error', reportConstantRedefinition: 'none', + reportDeprecated: 'none', reportIncompatibleMethodOverride: 'none', reportIncompatibleVariableOverride: 'none', reportInconsistentConstructor: 'none', @@ -605,6 +611,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet { reportTypeCommentUsage: 'error', reportPrivateImportUsage: 'error', reportConstantRedefinition: 'error', + reportDeprecated: 'error', reportIncompatibleMethodOverride: 'error', reportIncompatibleVariableOverride: 'error', reportInconsistentConstructor: 'error', diff --git a/packages/pyright-internal/src/common/diagnosticRules.ts b/packages/pyright-internal/src/common/diagnosticRules.ts index 5dd04ac01..99885d440 100644 --- a/packages/pyright-internal/src/common/diagnosticRules.ts +++ b/packages/pyright-internal/src/common/diagnosticRules.ts @@ -46,6 +46,7 @@ export enum DiagnosticRule { reportTypeCommentUsage = 'reportTypeCommentUsage', reportPrivateImportUsage = 'reportPrivateImportUsage', reportConstantRedefinition = 'reportConstantRedefinition', + reportDeprecated = 'reportDeprecated', reportIncompatibleMethodOverride = 'reportIncompatibleMethodOverride', reportIncompatibleVariableOverride = 'reportIncompatibleVariableOverride', reportInconsistentConstructor = 'reportInconsistentConstructor', diff --git a/packages/pyright-internal/src/localization/localize.ts b/packages/pyright-internal/src/localization/localize.ts index fa06dc41a..af8f698dc 100644 --- a/packages/pyright-internal/src/localization/localize.ts +++ b/packages/pyright-internal/src/localization/localize.ts @@ -329,6 +329,8 @@ export namespace Localizer { export const declaredReturnTypeUnknown = () => getRawString('Diagnostic.declaredReturnTypeUnknown'); export const defaultValueContainsCall = () => getRawString('Diagnostic.defaultValueContainsCall'); export const defaultValueNotAllowed = () => getRawString('Diagnostic.defaultValueNotAllowed'); + export const deprecatedClass = () => getRawString('Diagnostic.deprecatedClass'); + export const deprecatedFunction = () => getRawString('Diagnostic.deprecatedFunction'); export const deprecatedType = () => new ParameterizedString<{ version: string; replacement: string }>( getRawString('Diagnostic.deprecatedType') diff --git a/packages/pyright-internal/src/localization/package.nls.en-us.json b/packages/pyright-internal/src/localization/package.nls.en-us.json index a5306465c..915cd7463 100644 --- a/packages/pyright-internal/src/localization/package.nls.en-us.json +++ b/packages/pyright-internal/src/localization/package.nls.en-us.json @@ -89,6 +89,8 @@ "declaredReturnTypeUnknown": "Declared return type is unknown", "defaultValueContainsCall": "Function calls and mutable objects not allowed within parameter default value expression", "defaultValueNotAllowed": "Parameter with \"*\" or \"**\" cannot have default value", + "deprecatedClass": "This class is deprecated", + "deprecatedFunction": "This function is deprecated", "deprecatedType": "This type is deprecated as of Python {version}; use \"{replacement}\" instead", "delTargetExpr": "Expression cannot be deleted", "dictExpandIllegalInComprehension": "Dictionary expansion not allowed in comprehension", diff --git a/packages/pyright-internal/src/tests/checker.test.ts b/packages/pyright-internal/src/tests/checker.test.ts index 4319b91c0..2b35f283e 100644 --- a/packages/pyright-internal/src/tests/checker.test.ts +++ b/packages/pyright-internal/src/tests/checker.test.ts @@ -492,3 +492,25 @@ test('RegionComments1', () => { // const analysisResults3 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions); // TestUtils.validateResults(analysisResults3, 0, 0, 0, 0, 13); // }); + +test('Deprecated2', () => { + const configOptions = new ConfigOptions('.'); + + const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated2.py'], configOptions); + TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 5); + + configOptions.diagnosticRuleSet.reportDeprecated = 'error'; + const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated2.py'], configOptions); + TestUtils.validateResults(analysisResults2, 5); +}); + +test('Deprecated3', () => { + const configOptions = new ConfigOptions('.'); + + const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated3.py'], configOptions); + TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 5); + + configOptions.diagnosticRuleSet.reportDeprecated = 'error'; + const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated3.py'], configOptions); + TestUtils.validateResults(analysisResults2, 5); +}); diff --git a/packages/pyright-internal/src/tests/samples/deprecated2.py b/packages/pyright-internal/src/tests/samples/deprecated2.py new file mode 100644 index 000000000..01755d9db --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/deprecated2.py @@ -0,0 +1,71 @@ +# This sample tests the @typing.deprecated decorator introduced in PEP 702. + +from typing_extensions import deprecated, overload + + +@deprecated("Use ClassB instead") +class ClassA: + ... + + +# This should generate an error if reportDeprecated is enabled. +ClassA() + + +class ClassC: + @deprecated("Don't temp me") + def method1(self) -> None: + ... + + @overload + @deprecated("Int is no longer supported") + def method2(self, a: int) -> None: + ... + + @overload + def method2(self, a: None = None) -> None: + ... + + def method2(self, a: int | None = None) -> None: + ... + + +c1 = ClassC() + +# This should generate an error if reportDeprecated is enabled. +c1.method1() + +c1.method2() + +# This should generate an error if reportDeprecated is enabled. +c1.method2(2) + + +@deprecated("Test") +def func1() -> None: + ... + + +# This should generate an error if reportDeprecated is enabled. +func1() + + +@overload +def func2(a: str) -> None: + ... + + +@overload +@deprecated("int no longer supported") +def func2(a: int) -> int: + ... + + +def func2(a: str | int) -> int | None: + ... + + +func2("hi") + +# This should generate an error if reportDeprecated is enabled. +func2(3) diff --git a/packages/pyright-internal/src/tests/samples/deprecated3.py b/packages/pyright-internal/src/tests/samples/deprecated3.py new file mode 100644 index 000000000..954e0cd31 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/deprecated3.py @@ -0,0 +1,25 @@ +# This sample tests the @typing.deprecated decorator introduced in PEP 702. + +# This should generate an error if reportDeprecated is enabled. +from .deprecated2 import func1 + +# This should generate an error if reportDeprecated is enabled. +from .deprecated2 import ClassA as A + +from .deprecated2 import func2 +from .deprecated2 import ClassC as C + +func2("hi") + +# This should generate an error if reportDeprecated is enabled. +func2(1) + +# This should generate an error if reportDeprecated is enabled. +c1 = C.method1 + + +c2 = C() +c2.method2() + +# This should generate an error if reportDeprecated is enabled. +c2.method2(3) diff --git a/packages/pyright-internal/typeshed-fallback/stdlib/typing_extensions.pyi b/packages/pyright-internal/typeshed-fallback/stdlib/typing_extensions.pyi index 6513fbb86..e26b355f9 100644 --- a/packages/pyright-internal/typeshed-fallback/stdlib/typing_extensions.pyi +++ b/packages/pyright-internal/typeshed-fallback/stdlib/typing_extensions.pyi @@ -313,3 +313,9 @@ def override(__arg: _F) -> _F: ... # Proposed extension to PEP 647 StrictTypeGuard: _SpecialForm = ... + +# Proposed PEP 702 +@overload +def deprecated(__f: _F) -> _F: ... +@overload +def deprecated(__msg: str) -> Callable[[_F], _F]: ... diff --git a/packages/vscode-pyright/README.md b/packages/vscode-pyright/README.md index 2004b7835..a9fff9d87 100644 --- a/packages/vscode-pyright/README.md +++ b/packages/vscode-pyright/README.md @@ -36,6 +36,7 @@ Pyright supports [configuration files](/docs/configuration.md) that provide gran * [PEP 695](https://www.python.org/dev/peps/pep-0695/) (draft) Type parameter syntax * [PEP 696](https://www.python.org/dev/peps/pep-0696/) (draft) Type defaults for TypeVarLikes * [PEP 698](https://www.python.org/dev/peps/pep-0698/) (draft) Override decorator for static typing +* [PEP 702](https://www.python.org/dev/peps/pep-0702/) (draft) Marking deprecations * Type inference for function return values, instance variables, class variables, and globals * Type guards that understand conditional code flow constructs like if/else statements diff --git a/packages/vscode-pyright/package.json b/packages/vscode-pyright/package.json index b051385a2..ecc879d48 100644 --- a/packages/vscode-pyright/package.json +++ b/packages/vscode-pyright/package.json @@ -473,6 +473,17 @@ "error" ] }, + "reportDeprecated": { + "type": "string", + "description": "Diagnostics for use of deprecated classes or functions.", + "default": "none", + "enum": [ + "none", + "information", + "warning", + "error" + ] + }, "reportIncompatibleMethodOverride": { "type": "string", "description": "Diagnostics for methods that override a method of the same name in a base class in an incompatible manner (wrong number of parameters, incompatible parameter types, or incompatible return type).", diff --git a/packages/vscode-pyright/schemas/pyrightconfig.schema.json b/packages/vscode-pyright/schemas/pyrightconfig.schema.json index 25bfd0693..6845f9551 100644 --- a/packages/vscode-pyright/schemas/pyrightconfig.schema.json +++ b/packages/vscode-pyright/schemas/pyrightconfig.schema.json @@ -316,6 +316,12 @@ "title": "Controls reporting of attempts to redefine variables that are in all-caps", "default": "none" }, + "reportDeprecated": { + "$id": "#/properties/reportDeprecated", + "$ref": "#/definitions/diagnostic", + "title": "Controls reporting of use of deprecated class or function", + "default": "none" + }, "reportIncompatibleMethodOverride": { "$id": "#/properties/reportIncompatibleMethodOverride", "$ref": "#/definitions/diagnostic",