Added "reportTypedDictNotRequiredAccess" diagnostic rule and split out diagnostics that pertain specifically to unguarded accesses to non-required TypedDict keys.

This commit is contained in:
Eric Traut 2021-03-27 09:28:28 -07:00
parent 41a0bc4015
commit ea8628909a
8 changed files with 54 additions and 13 deletions

View File

@ -86,6 +86,8 @@ The following settings control pyrights diagnostic output (warnings or errors
**reportOptionalOperand** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to use an Optional type as an operand to a binary or unary operator (like '+', '==', 'or', 'not'). The default value for this setting is 'none'.
**reportTypedDictNotRequiredAccess** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to access a non-required field within a TypedDict without first checking whether it is present. The default value for this setting is 'error'.
**reportUntypedFunctionDecorator** [boolean or string, optional]: Generate or suppress diagnostics for function decorators that have no type annotations. These obscure the function type, defeating many type analysis features. The default value for this setting is 'none'.
**reportUntypedClassDecorator** [boolean or string, optional]: Generate or suppress diagnostics for class decorators that have no type annotations. These obscure the class type, defeating many type analysis features. The default value for this setting is 'none'.
@ -249,6 +251,7 @@ The following table lists the default severity levels for each diagnostic rule w
| reportOptionalIterable | "none" | "none" | "error" |
| reportOptionalContextManager | "none" | "none" | "error" |
| reportOptionalOperand | "none" | "none" | "error" |
| reportTypedDictNotRequiredAccess | "none" | "error" | "error" |
| reportUntypedFunctionDecorator | "none" | "none" | "error" |
| reportUntypedClassDecorator | "none" | "none" | "error" |
| reportUntypedBaseClass | "none" | "none" | "error" |
@ -257,7 +260,7 @@ The following table lists the default severity levels for each diagnostic rule w
| reportConstantRedefinition | "none" | "none" | "error" |
| reportIncompatibleMethodOverride | "none" | "none" | "error" |
| reportIncompatibleVariableOverride | "none" | "none" | "error" |
| reportOverlappingOverload | "none" | "none" | "error" |
| reportOverlappingOverload | "none" | "none" | "error" |
| reportInvalidStringEscapeSequence | "none" | "warning" | "error" |
| reportUnknownParameterType | "none" | "none" | "error" |
| reportUnknownArgumentType | "none" | "none" | "error" |

View File

@ -5463,6 +5463,7 @@ export function createTypeEvaluator(
const indexTypeResult = getTypeOfExpression(node.items[0].valueExpression);
const indexType = indexTypeResult.type;
let diag = new DiagnosticAddendum();
let allDiagsInvolveNotRequiredKeys = true;
const resultingType = mapSubtypes(indexType, (subtype) => {
if (isAnyOrUnknown(subtype)) {
@ -5486,6 +5487,7 @@ export function createTypeEvaluator(
type: printType(baseType),
})
);
allDiagsInvolveNotRequiredKeys = false;
return UnknownType.create();
} else if (!entry.isRequired && usage.method === 'get') {
diag.addMessage(
@ -5499,19 +5501,19 @@ export function createTypeEvaluator(
if (usage.method === 'set') {
canAssignType(entry.valueType, usage.setType!, diag);
} else if (usage.method === 'del' && entry.isRequired) {
const fileInfo = getFileInfo(node);
addDiagnostic(
fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
DiagnosticRule.reportGeneralTypeIssues,
Localizer.Diagnostic.keyRequiredDeleted().format({ name: entryName }),
node
diag.addMessage(
Localizer.DiagnosticAddendum.keyRequiredDeleted().format({
name: entryName,
})
);
allDiagsInvolveNotRequiredKeys = false;
}
return entry.valueType;
}
diag.addMessage(Localizer.DiagnosticAddendum.typeNotStringLiteral().format({ type: printType(subtype) }));
allDiagsInvolveNotRequiredKeys = false;
return UnknownType.create();
});
@ -5534,8 +5536,12 @@ export function createTypeEvaluator(
const fileInfo = getFileInfo(node);
addDiagnostic(
fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
DiagnosticRule.reportGeneralTypeIssues,
allDiagsInvolveNotRequiredKeys
? fileInfo.diagnosticRuleSet.reportTypedDictNotRequiredAccess
: fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
allDiagsInvolveNotRequiredKeys
? DiagnosticRule.reportTypedDictNotRequiredAccess
: DiagnosticRule.reportGeneralTypeIssues,
typedDictDiag + diag.getString(),
node
);

View File

@ -147,9 +147,12 @@ export interface DiagnosticRuleSet {
// Report attempts to use an Optional type in a "with" statement?
reportOptionalContextManager: DiagnosticLevel;
// Report attempts to use an Optional type in a binary or unary operation.
// Report attempts to use an Optional type in a binary or unary operation?
reportOptionalOperand: DiagnosticLevel;
// Report accesses to non-required TypedDict fields?
reportTypedDictNotRequiredAccess: DiagnosticLevel;
// Report untyped function decorators that obscure the function type?
reportUntypedFunctionDecorator: DiagnosticLevel;
@ -288,6 +291,7 @@ export function getDiagLevelDiagnosticRules() {
DiagnosticRule.reportOptionalIterable,
DiagnosticRule.reportOptionalContextManager,
DiagnosticRule.reportOptionalOperand,
DiagnosticRule.reportTypedDictNotRequiredAccess,
DiagnosticRule.reportUntypedFunctionDecorator,
DiagnosticRule.reportUntypedClassDecorator,
DiagnosticRule.reportUntypedBaseClass,
@ -355,6 +359,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet {
reportOptionalIterable: 'none',
reportOptionalContextManager: 'none',
reportOptionalOperand: 'none',
reportTypedDictNotRequiredAccess: 'none',
reportUntypedFunctionDecorator: 'none',
reportUntypedClassDecorator: 'none',
reportUntypedBaseClass: 'none',
@ -418,6 +423,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet {
reportOptionalIterable: 'none',
reportOptionalContextManager: 'none',
reportOptionalOperand: 'none',
reportTypedDictNotRequiredAccess: 'error',
reportUntypedFunctionDecorator: 'none',
reportUntypedClassDecorator: 'none',
reportUntypedBaseClass: 'none',
@ -481,6 +487,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
reportOptionalIterable: 'error',
reportOptionalContextManager: 'error',
reportOptionalOperand: 'error',
reportTypedDictNotRequiredAccess: 'error',
reportUntypedFunctionDecorator: 'error',
reportUntypedClassDecorator: 'error',
reportUntypedBaseClass: 'error',
@ -960,6 +967,13 @@ export class ConfigOptions {
defaultSettings.reportOptionalOperand
),
// Read the "reportTypedDictNotRequiredAccess" entry.
reportTypedDictNotRequiredAccess: this._convertDiagnosticLevel(
configObj.reportTypedDictNotRequiredAccess,
DiagnosticRule.reportTypedDictNotRequiredAccess,
defaultSettings.reportTypedDictNotRequiredAccess
),
// Read the "reportUntypedFunctionDecorator" entry.
reportUntypedFunctionDecorator: this._convertDiagnosticLevel(
configObj.reportUntypedFunctionDecorator,

View File

@ -35,6 +35,7 @@ export enum DiagnosticRule {
reportOptionalIterable = 'reportOptionalIterable',
reportOptionalContextManager = 'reportOptionalContextManager',
reportOptionalOperand = 'reportOptionalOperand',
reportTypedDictNotRequiredAccess = 'reportTypedDictNotRequiredAccess',
reportUntypedFunctionDecorator = 'reportUntypedFunctionDecorator',
reportUntypedClassDecorator = 'reportUntypedClassDecorator',
reportUntypedBaseClass = 'reportUntypedBaseClass',

View File

@ -388,8 +388,6 @@ export namespace Localizer {
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.isInstanceInvalidType'));
export const isSubclassInvalidType = () =>
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.isSubclassInvalidType'));
export const keyRequiredDeleted = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.keyRequiredDeleted'));
export const keyValueInSet = () => getRawString('Diagnostic.keyValueInSet');
export const keywordArgInTypeArgument = () => getRawString('Diagnostic.keywordArgInTypeArgument');
export const keywordSubscriptIllegal = () => getRawString('Diagnostic.keywordSubscriptIllegal');
@ -831,6 +829,8 @@ export namespace Localizer {
);
export const keyNotRequired = () =>
new ParameterizedString<{ name: string; type: string }>(getRawString('DiagnosticAddendum.keyNotRequired'));
export const keyRequiredDeleted = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.keyRequiredDeleted'));
export const keyUndefined = () =>
new ParameterizedString<{ name: string; type: string }>(getRawString('DiagnosticAddendum.keyUndefined'));
export const literalAssignmentMismatch = () =>

View File

@ -172,7 +172,6 @@
"invalidTokenChars": "Invalid character in token \"{text}\"",
"isInstanceInvalidType": "Second argument to \"isinstance\" must be a class or tuple of classes",
"isSubclassInvalidType": "Second argument to \"issubclass\" must be a class or tuple of classes",
"keyRequiredDeleted": "\"{name}\" is a required key and cannot be deleted",
"keyValueInSet": "Key/value pairs are not allowed within a set",
"keywordArgInTypeArgument": "Keyword arguments cannot be used in type argument lists",
"keywordSubscriptIllegal": "Keyword arguments within subscripts requires Python 3.10 or newer",
@ -430,6 +429,7 @@
"dataclassFrozen": "Dataclass \"{name}\" is frozen",
"finalMethod": "Final method",
"keyNotRequired": "\"{name}\" is not a required key in \"{type}\", so access may result in runtime exception",
"keyRequiredDeleted": "\"{name}\" is a required key and cannot be deleted",
"keyUndefined": "\"{name}\" is not a defined key in \"{type}\"",
"functionParamName": "Parameter name mismatch: \"{destName}\" versus \"{srcName}\"",
"functionReturnTypeMismatch": "Function return type \"{sourceType}\" is incompatible with type \"{destType}\"",

View File

@ -323,6 +323,17 @@
"error"
]
},
"reportTypedDictNotRequiredAccess": {
"type": "string",
"description": "Diagnostics for an attempt to access a non-required key within a TypedDict without a check for its presence.",
"default": "error",
"enum": [
"none",
"information",
"warning",
"error"
]
},
"reportUntypedFunctionDecorator": {
"type": "string",
"description": "Diagnostics for function decorators that have no type annotations. These obscure the function type, defeating many type analysis features.",

View File

@ -239,6 +239,12 @@
"title": "Controls reporting of attempts to use an Optional type as an operand for a binary or unary operator",
"default": "none"
},
"reportTypedDictNotRequiredAccess": {
"$id": "#/properties/reportTypedDictNotRequiredAccess",
"$ref": "#/definitions/diagnostic",
"title": "Controls reporting of attempts to access a non-required key in a TypedDict without a check for its presence",
"default": "error"
},
"reportUntypedFunctionDecorator": {
"$id": "#/properties/reportUntypedFunctionDecorator",
"$ref": "#/definitions/diagnostic",