Added support for new reportUnsupportedDunderAll diagnostic rule. It checks for unsupported manipulations of __all__.

This commit is contained in:
Eric Traut 2020-11-25 16:07:59 -08:00
parent b5240edebc
commit a44e254c32
10 changed files with 135 additions and 1 deletions

View File

@ -132,6 +132,8 @@ The following settings control pyrights diagnostic output (warnings or errors
**reportInvalidStubStatement** [boolean or string, optional]: Generate or suppress diagnostics for statements that are syntactically correct but have no purpose within a type stub file. The default value for this setting is 'none'.
**reportUnsupportedDunderAll** [boolean or string, optional]: Generate or suppress diagnostics for statements that define or manipulate `__all__` in a way that is not allowed by a static type checker, thus rendering the contents of `__all__` to be unknown or incorrect. The default value for this setting is 'warning'.
## Execution Environment Options
Pyright allows multiple “execution environments” to be defined for different portions of your source tree. For example, a subtree may be designed to run with different import search paths or a different version of the python interpreter than the rest of the source base.

View File

@ -567,6 +567,8 @@ export class Binder extends ParseTreeWalker {
node.leftExpression.leftExpression.nodeType === ParseNodeType.Name &&
node.leftExpression.leftExpression.value === '__all__'
) {
let emitDunderAllWarning = true;
// Is this a call to "__all__.extend()"?
if (node.leftExpression.memberName.value === 'extend' && node.arguments.length === 1) {
const argExpr = node.arguments[0].valueExpression;
@ -580,6 +582,7 @@ export class Binder extends ParseTreeWalker {
listEntryNode.strings[0].nodeType === ParseNodeType.String
) {
this._dunderAllNames?.push(listEntryNode.strings[0].value);
emitDunderAllWarning = false;
}
});
} else if (
@ -589,10 +592,11 @@ export class Binder extends ParseTreeWalker {
) {
// Is this a call to "__all__.extend(<mod>.__all__)"?
const namesToAdd = this._getDunderAllNamesFromImport(argExpr.leftExpression.value);
if (namesToAdd) {
if (namesToAdd && namesToAdd.length > 0) {
namesToAdd.forEach((name) => {
this._dunderAllNames?.push(name);
});
emitDunderAllWarning = false;
}
}
} else if (node.leftExpression.memberName.value === 'remove' && node.arguments.length === 1) {
@ -605,6 +609,7 @@ export class Binder extends ParseTreeWalker {
this._dunderAllNames
) {
this._dunderAllNames = this._dunderAllNames.filter((name) => name !== argExpr.strings[0].value);
emitDunderAllWarning = false;
}
} else if (node.leftExpression.memberName.value === 'append' && node.arguments.length === 1) {
// Is this a call to "__all__.append()"?
@ -615,8 +620,18 @@ export class Binder extends ParseTreeWalker {
argExpr.strings[0].nodeType === ParseNodeType.String
) {
this._dunderAllNames?.push(argExpr.strings[0].value);
emitDunderAllWarning = false;
}
}
if (emitDunderAllWarning) {
this._addDiagnostic(
this._fileInfo.diagnosticRuleSet.reportUnsupportedDunderAll,
DiagnosticRule.reportUnsupportedDunderAll,
Localizer.Diagnostic.unsupportedDunderAllOperation(),
node
);
}
}
return false;
@ -661,6 +676,7 @@ export class Binder extends ParseTreeWalker {
) {
const expr = node.rightExpression;
this._dunderAllNames = [];
let emitDunderAllWarning = false;
if (expr.nodeType === ParseNodeType.List) {
expr.entries.forEach((listEntryNode) => {
@ -670,6 +686,8 @@ export class Binder extends ParseTreeWalker {
listEntryNode.strings[0].nodeType === ParseNodeType.String
) {
this._dunderAllNames!.push(listEntryNode.strings[0].value);
} else {
emitDunderAllWarning = true;
}
});
} else if (expr.nodeType === ParseNodeType.Tuple) {
@ -680,8 +698,21 @@ export class Binder extends ParseTreeWalker {
tupleEntryNode.strings[0].nodeType === ParseNodeType.String
) {
this._dunderAllNames!.push(tupleEntryNode.strings[0].value);
} else {
emitDunderAllWarning = true;
}
});
} else {
emitDunderAllWarning = true;
}
if (emitDunderAllWarning) {
this._addDiagnostic(
this._fileInfo.diagnosticRuleSet.reportUnsupportedDunderAll,
DiagnosticRule.reportUnsupportedDunderAll,
Localizer.Diagnostic.unsupportedDunderAllOperation(),
node
);
}
}
}
@ -751,6 +782,7 @@ export class Binder extends ParseTreeWalker {
node.leftExpression.value === '__all__'
) {
const expr = node.rightExpression;
let emitDunderAllWarning = true;
if (expr.nodeType === ParseNodeType.List) {
// Is this the form __all__ += ["a", "b"]?
@ -763,6 +795,7 @@ export class Binder extends ParseTreeWalker {
this._dunderAllNames?.push(listEntryNode.strings[0].value);
}
});
emitDunderAllWarning = false;
} else if (
expr.nodeType === ParseNodeType.MemberAccess &&
expr.leftExpression.nodeType === ParseNodeType.Name &&
@ -774,8 +807,19 @@ export class Binder extends ParseTreeWalker {
namesToAdd.forEach((name) => {
this._dunderAllNames?.push(name);
});
emitDunderAllWarning = false;
}
}
if (emitDunderAllWarning) {
this._addDiagnostic(
this._fileInfo.diagnosticRuleSet.reportUnsupportedDunderAll,
DiagnosticRule.reportUnsupportedDunderAll,
Localizer.Diagnostic.unsupportedDunderAllOperation(),
node
);
}
}
return false;

View File

@ -225,6 +225,10 @@ export interface DiagnosticRuleSet {
// Report statements that are syntactically correct but
// have no semantic meaning within a type stub file.
reportInvalidStubStatement: DiagnosticLevel;
// Report operations on __all__ symbol that are not supported
// by a static type checker.
reportUnsupportedDunderAll: DiagnosticLevel;
}
export function cloneDiagnosticRuleSet(diagSettings: DiagnosticRuleSet): DiagnosticRuleSet {
@ -290,6 +294,7 @@ export function getDiagLevelDiagnosticRules() {
DiagnosticRule.reportUndefinedVariable,
DiagnosticRule.reportUnboundVariable,
DiagnosticRule.reportInvalidStubStatement,
DiagnosticRule.reportUnsupportedDunderAll,
];
}
@ -352,6 +357,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet {
reportUnboundVariable: 'warning',
reportUndefinedVariable: 'warning',
reportInvalidStubStatement: 'none',
reportUnsupportedDunderAll: 'none',
};
return diagSettings;
@ -410,6 +416,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet {
reportUnboundVariable: 'error',
reportUndefinedVariable: 'error',
reportInvalidStubStatement: 'none',
reportUnsupportedDunderAll: 'warning',
};
return diagSettings;
@ -468,6 +475,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
reportUnboundVariable: 'error',
reportUndefinedVariable: 'error',
reportInvalidStubStatement: 'error',
reportUnsupportedDunderAll: 'error',
};
return diagSettings;
@ -1096,6 +1104,13 @@ export class ConfigOptions {
DiagnosticRule.reportInvalidStubStatement,
defaultSettings.reportInvalidStubStatement
),
// Read the "reportUnsupportedDunderAll" entry.
reportUnsupportedDunderAll: this._convertDiagnosticLevel(
configObj.reportUnsupportedDunderAll,
DiagnosticRule.reportUnsupportedDunderAll,
defaultSettings.reportUnsupportedDunderAll
),
};
// Read the "venvPath".

View File

@ -59,4 +59,5 @@ export enum DiagnosticRule {
reportUndefinedVariable = 'reportUndefinedVariable',
reportUnboundVariable = 'reportUnboundVariable',
reportInvalidStubStatement = 'reportInvalidStubStatement',
reportUnsupportedDunderAll = 'reportUnsupportedDunderAll',
}

View File

@ -655,6 +655,8 @@ export namespace Localizer {
export const unpackInSet = () => getRawString('Diagnostic.unpackInSet');
export const unpackTuplesIllegal = () => getRawString('Diagnostic.unpackTuplesIllegal');
export const unreachableCode = () => getRawString('Diagnostic.unreachableCode');
export const unsupportedDunderAllAssignment = () => getRawString('Diagnostic.unsupportedDunderAllAssignment');
export const unsupportedDunderAllOperation = () => getRawString('Diagnostic.unsupportedDunderAllOperation');
export const varAnnotationIllegal = () => getRawString('Diagnostic.varAnnotationIllegal');
export const walrusIllegal = () => getRawString('Diagnostic.walrusIllegal');
export const walrusNotAllowed = () => getRawString('Diagnostic.walrusNotAllowed');

View File

@ -326,6 +326,8 @@
"unpackInSet": "Unpack operator not allowed within a set",
"unpackTuplesIllegal": "Unpack operation not allowed in tuples prior to Python 3.8",
"unreachableCode": "Code is unreachable",
"unsupportedDunderAllAssignment": "Expression assigned to \"__all__\" is not supported, so exported symbol list may be incorrect; use list or tuple of string literal values in assignment",
"unsupportedDunderAllOperation": "Operation on \"__all__\" is not supported, so exported symbol list may not be incorrect",
"varAnnotationIllegal": "Type annotations for variables requires Python 3.6 or newer; use type comment for compatibility with previous versions",
"walrusIllegal": "Operator \":=\" requires Python 3.8 or newer",
"walrusNotAllowed": "Operator \":=\" not allowed in this context",

View File

@ -0,0 +1,33 @@
# This sample tests the reportUnsupportedDunderAll diagnostic rule.
# pyright: reportMissingModuleSource=false
from typing import Any
import mock
__all__: Any
__all__ = ("test", "hello")
__all__ = ["test", "hello"]
__all__.append("foo")
__all__.extend(["foo"])
__all__.remove("foo")
__all__ += ["bar"]
__all__ += mock.__all__
__all__.extend(mock.__all__)
my_string = "foo"
# The following should all generate diagnostics if reportUnsupportedDunderAll
# is enabled.
__all__ = ("test", my_string)
__all__ = ["test", my_string]
__all__ = "test"
__all__.append(my_string)
__all__.extend([my_string])
__all__.remove(my_string)
__all__ += [my_string]
__all__ += mock.AsyncMock
__all__.extend(mock.AsyncMock)
__all__.something()

View File

@ -722,6 +722,24 @@ test('Import12', () => {
TestUtils.validateResults(analysisResults, 0, 0);
});
test('DunderAll1', () => {
const configOptions = new ConfigOptions('.');
// By default, reportUnsupportedDunderAll is a warning.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['dunderAll1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0, 9);
// Turn on error.
configOptions.diagnosticRuleSet.reportUnsupportedDunderAll = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['dunderAll1.py'], configOptions);
TestUtils.validateResults(analysisResults, 9, 0);
// Turn off diagnostic.
configOptions.diagnosticRuleSet.reportUnsupportedDunderAll = 'none';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['dunderAll1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0, 0);
});
test('Overload1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['overload1.py']);
TestUtils.validateResults(analysisResults, 2);

View File

@ -586,6 +586,17 @@
"warning",
"error"
]
},
"reportUnsupportedDunderAll": {
"type": "string",
"description": "Diagnostics for unsupported operations performed on __all__.",
"default": "warning",
"enum": [
"none",
"information",
"warning",
"error"
]
}
}
},

View File

@ -381,6 +381,12 @@
"title": "Controls reporting of type stub statements that do not conform to PEP 484",
"default": "none"
},
"reportUnsupportedDunderAll": {
"$id": "#/properties/reportUnsupportedDunderAll",
"$ref": "#/definitions/diagnostic",
"title": "Controls reporting of unsupported operations performed on __all__",
"default": "warning"
},
"pythonVersion": {
"$id": "#/properties/pythonVersion",
"type": "string",