Added new configuration setting called "defineConstant". It allows a configuration to specify one or more identifiers that should be assigned by pyright's binder to be constant anywhere they appear. Values can be boolean (true or false) or a string. If an identifier of this value is used within a conditional statement (like if not DEBUG:) it will affect pyright's reachability analysis for the guarded code blocks.

This commit is contained in:
Eric Traut 2022-06-08 22:01:33 -07:00
parent 9b407f6c9e
commit 794d151050
13 changed files with 186 additions and 11 deletions

View File

@ -16,6 +16,8 @@ Relative paths specified within the config file are relative to the config file
**strict** [array of paths, optional]: Paths of directories or files that should use “strict” analysis if they are included. This is the same as manually adding a “# pyright: strict” comment. In strict mode, most type-checking rules are enabled. Refer to [this table](https://github.com/microsoft/pyright/blob/main/docs/configuration.md#diagnostic-rule-defaults) for details about which rules are enabled in strict mode. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character).
**defineConstant** [map of constants to values (boolean or string), optional]: Set of identifiers that should be assumed to contain a constant value wherever used within this program. For example, `{ "DEBUG": true }` indicates that pyright should assume that the identifier `DEBUG` will always be equal to `True`. If this identifier is used within a conditional expression (such as `if not DEBUG:`) pyright will use the indicated value to determine whether the guarded block is reachable or not.
**typeshedPath** [path, optional]: Path to a directory that contains typeshed type stub files. Pyright ships with a bundled copy of typeshed type stubs. If you want to use a different version of typeshed stubs, you can clone the [typeshed github repo](https://github.com/python/typeshed) to a local directory and reference the location with this path. This option is useful if youre actively contributing updates to typeshed.
**stubPath** [path, optional]: Path to a directory that contains custom type stubs. Each package's type stub file(s) are expected to be in its own subdirectory. The default value of this setting is "./typings". (typingsPath is now deprecated)
@ -204,6 +206,10 @@ The following is an example of a pyright config file:
"src/oldstuff"
],
"defineConstant": {
"DEBUG": true
},
"stubPath": "src/stubs",
"venv": "env367",
@ -253,6 +259,7 @@ exclude = ["**/node_modules",
"src/typestubs"
]
ignore = ["src/oldstuff"]
defineConstant = { DEBUG = true }
stubPath = "src/stubs"
venv = "env367"

View File

@ -39,6 +39,7 @@ export interface AnalyzerFileInfo {
fileContents: string;
lines: TextRangeCollection<TextRange>;
typingSymbolAliases: Map<string, string>;
definedConstants: Map<string, boolean | string>;
filePath: string;
moduleName: string;
isStubFile: boolean;

View File

@ -1132,6 +1132,7 @@ export class Binder extends ParseTreeWalker {
const constExprValue = StaticExpressions.evaluateStaticBoolLikeExpression(
node.testExpression,
this._fileInfo.executionEnvironment,
this._fileInfo.definedConstants,
this._typingImportAliases,
this._sysImportAliases
);
@ -1171,6 +1172,7 @@ export class Binder extends ParseTreeWalker {
const constExprValue = StaticExpressions.evaluateStaticBoolLikeExpression(
node.testExpression,
this._fileInfo.executionEnvironment,
this._fileInfo.definedConstants,
this._typingImportAliases,
this._sysImportAliases
);
@ -2634,6 +2636,7 @@ export class Binder extends ParseTreeWalker {
const staticValue = StaticExpressions.evaluateStaticBoolLikeExpression(
expression,
this._fileInfo.executionEnvironment,
this._fileInfo.definedConstants,
this._typingImportAliases,
this._sysImportAliases
);

View File

@ -1499,7 +1499,13 @@ export class Checker extends ParseTreeWalker {
// Check for the special case where the LHS and RHS are both literals.
if (isLiteralTypeOrUnion(rightType) && isLiteralTypeOrUnion(leftType)) {
if (evaluateStaticBoolExpression(node, this._fileInfo.executionEnvironment) === undefined) {
if (
evaluateStaticBoolExpression(
node,
this._fileInfo.executionEnvironment,
this._fileInfo.definedConstants
) === undefined
) {
let isPossiblyTrue = false;
doForEachSubtype(leftType, (leftSubtype) => {

View File

@ -188,9 +188,11 @@ export function synthesizeDataClassMethods(
(arg) => arg.name?.value === 'init'
);
if (initArg && initArg.valueExpression) {
const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
const value = evaluateStaticBoolExpression(
initArg.valueExpression,
AnalyzerNodeInfo.getFileInfo(node).executionEnvironment
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (value === false) {
includeInInit = false;
@ -242,9 +244,11 @@ export function synthesizeDataClassMethods(
(arg) => arg.name?.value === 'kw_only'
);
if (kwOnlyArg && kwOnlyArg.valueExpression) {
const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
const value = evaluateStaticBoolExpression(
kwOnlyArg.valueExpression,
AnalyzerNodeInfo.getFileInfo(node).executionEnvironment
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (value === false) {
isKeywordOnly = false;
@ -705,7 +709,11 @@ export function validateDataClassTransformDecorator(
switch (arg.name.value) {
case 'kw_only_default': {
const value = evaluateStaticBoolExpression(arg.valueExpression, fileInfo.executionEnvironment);
const value = evaluateStaticBoolExpression(
arg.valueExpression,
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (value === undefined) {
evaluator.addError(
Localizer.Diagnostic.dataClassTransformExpectedBoolLiteral(),
@ -719,7 +727,11 @@ export function validateDataClassTransformDecorator(
}
case 'eq_default': {
const value = evaluateStaticBoolExpression(arg.valueExpression, fileInfo.executionEnvironment);
const value = evaluateStaticBoolExpression(
arg.valueExpression,
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (value === undefined) {
evaluator.addError(
Localizer.Diagnostic.dataClassTransformExpectedBoolLiteral(),
@ -733,7 +745,11 @@ export function validateDataClassTransformDecorator(
}
case 'order_default': {
const value = evaluateStaticBoolExpression(arg.valueExpression, fileInfo.executionEnvironment);
const value = evaluateStaticBoolExpression(
arg.valueExpression,
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (value === undefined) {
evaluator.addError(
Localizer.Diagnostic.dataClassTransformExpectedBoolLiteral(),
@ -839,7 +855,7 @@ function applyDataClassBehaviorOverride(
argValue: ExpressionNode
) {
const fileInfo = AnalyzerNodeInfo.getFileInfo(errorNode);
const value = evaluateStaticBoolExpression(argValue, fileInfo.executionEnvironment);
const value = evaluateStaticBoolExpression(argValue, fileInfo.executionEnvironment, fileInfo.definedConstants);
switch (argName) {
case 'order':

View File

@ -1301,6 +1301,7 @@ export class SourceFile {
fileContents,
lines: this._parseResults!.tokenizerOutput.lines,
typingSymbolAliases: this._parseResults!.typingSymbolAliases,
definedConstants: configOptions.defineConstant,
filePath: this._filePath,
moduleName: this._moduleName,
isStubFile: this._isStubFile,

View File

@ -17,11 +17,18 @@ import { KeywordType, OperatorType } from '../parser/tokenizerTypes';
export function evaluateStaticBoolExpression(
node: ExpressionNode,
execEnv: ExecutionEnvironment,
definedConstants: Map<string, boolean | string>,
typingImportAliases?: string[],
sysImportAliases?: string[]
): boolean | undefined {
if (node.nodeType === ParseNodeType.AssignmentExpression) {
return evaluateStaticBoolExpression(node.rightExpression, execEnv, typingImportAliases, sysImportAliases);
return evaluateStaticBoolExpression(
node.rightExpression,
execEnv,
definedConstants,
typingImportAliases,
sysImportAliases
);
}
if (node.nodeType === ParseNodeType.UnaryOperation) {
@ -29,6 +36,7 @@ export function evaluateStaticBoolExpression(
const value = evaluateStaticBoolLikeExpression(
node.expression,
execEnv,
definedConstants,
typingImportAliases,
sysImportAliases
);
@ -42,12 +50,14 @@ export function evaluateStaticBoolExpression(
const leftValue = evaluateStaticBoolExpression(
node.leftExpression,
execEnv,
definedConstants,
typingImportAliases,
sysImportAliases
);
const rightValue = evaluateStaticBoolExpression(
node.rightExpression,
execEnv,
definedConstants,
typingImportAliases,
sysImportAliases
);
@ -106,6 +116,18 @@ export function evaluateStaticBoolExpression(
if (expectedOsName !== undefined) {
return _evaluateStringBinaryOperation(node.operator, expectedOsName, comparisonOsName);
}
} else {
// Handle the special case of <definedConstant> == 'X' or <definedConstant> != 'X'.
if (
node.leftExpression.nodeType === ParseNodeType.Name &&
node.rightExpression.nodeType === ParseNodeType.StringList
) {
const constantValue = definedConstants.get(node.leftExpression.value);
if (constantValue !== undefined && typeof constantValue === 'string') {
const comparisonStringName = node.rightExpression.strings.map((s) => s.value).join('');
return _evaluateStringBinaryOperation(node.operator, constantValue, comparisonStringName);
}
}
}
} else if (node.nodeType === ParseNodeType.Constant) {
if (node.constType === KeywordType.True) {
@ -117,6 +139,11 @@ export function evaluateStaticBoolExpression(
if (node.value === 'TYPE_CHECKING') {
return true;
}
const constant = definedConstants.get(node.value);
if (constant !== undefined) {
return !!constant;
}
} else if (
typingImportAliases &&
node.nodeType === ParseNodeType.MemberAccess &&
@ -136,6 +163,7 @@ export function evaluateStaticBoolExpression(
export function evaluateStaticBoolLikeExpression(
node: ExpressionNode,
execEnv: ExecutionEnvironment,
definedConstants: Map<string, boolean | string>,
typingImportAliases?: string[],
sysImportAliases?: string[]
): boolean | undefined {
@ -145,7 +173,7 @@ export function evaluateStaticBoolLikeExpression(
}
}
return evaluateStaticBoolExpression(node, execEnv, typingImportAliases, sysImportAliases);
return evaluateStaticBoolExpression(node, execEnv, definedConstants, typingImportAliases, sysImportAliases);
}
function _convertTupleToVersion(node: TupleNode): number | undefined {

View File

@ -14203,7 +14203,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
// If the RHS is a constant boolean expression, assign it a literal type.
const constExprValue = evaluateStaticBoolExpression(
node.rightExpression,
fileInfo.executionEnvironment
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (constExprValue !== undefined) {
@ -14587,7 +14588,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
} else if (arg.name.value === 'total' && ClassType.isTypedDictClass(classType)) {
// The "total" parameter name applies only for TypedDict classes.
// PEP 589 specifies that the parameter must be either True or False.
const constArgValue = evaluateStaticBoolExpression(arg.valueExpression, fileInfo.executionEnvironment);
const constArgValue = evaluateStaticBoolExpression(
arg.valueExpression,
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (constArgValue === undefined) {
addError(Localizer.Diagnostic.typedDictTotalParam(), arg.valueExpression);
} else if (!constArgValue) {

View File

@ -663,6 +663,10 @@ export class ConfigOptions {
// A list of file specs that should be analyzed using "strict" mode.
strict: FileSpec[] = [];
// A set of defined constants that are used by the binder to determine
// whether runtime conditions should evaluate to True or False.
defineConstant = new Map<string, boolean | string>();
// Emit verbose information to console?
verboseOutput?: boolean | undefined;
@ -1029,6 +1033,24 @@ export class ConfigOptions {
}
}
// Read the "defineConstant" setting.
if (configObj.defineConstant !== undefined) {
if (typeof configObj.defineConstant !== 'object' || Array.isArray(configObj.defineConstant)) {
console.error(`Config "defineConstant" field must contain a map indexed by constant names.`);
} else {
const keys = Object.getOwnPropertyNames(configObj.defineConstant);
keys.forEach((key) => {
const value = configObj.defineConstant[key];
const valueType = typeof value;
if (valueType !== 'boolean' && valueType !== 'string') {
console.error(`Defined constant "${key}" must be associated with a boolean or string value.`);
} else {
this.defineConstant.set(key, value);
}
});
}
}
// Read the "useLibraryCodeForTypes" setting.
if (configObj.useLibraryCodeForTypes !== undefined) {
if (typeof configObj.useLibraryCodeForTypes !== 'boolean') {

View File

@ -0,0 +1,52 @@
# This sample tests static expression forms that are supported
# in the binder.
import sys
import os
x: int
if sys.platform == "linux":
x = 1
else:
x = "error!"
if sys.version_info >= (3, 9):
x = 1
else:
x = "error!"
if os.name == "posix":
x = 1
else:
x = "error!"
if True:
x = 1
else:
x = "error!"
if not False:
x = 1
else:
x = "error!"
DEFINED_TRUE = True
DEFINED_FALSE = False
if DEFINED_TRUE:
x = 1
else:
x = "error!"
if not DEFINED_FALSE:
x = 1
else:
x = "error!"
DEFINED_STR = "hi!"
if DEFINED_STR == "hi!":
x = 1
else:
x = "error!"

View File

@ -108,6 +108,7 @@ export function buildAnalyzerFileInfo(
diagnosticSink: analysisDiagnostics,
executionEnvironment: configOptions.findExecEnvironment(filePath),
diagnosticRuleSet: cloneDiagnosticRuleSet(configOptions.diagnosticRuleSet),
definedConstants: configOptions.defineConstant,
fileContents,
lines: parseResults.tokenizerOutput.lines,
filePath,

View File

@ -1331,3 +1331,25 @@ test('Dictionary4', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('StaticExpressions1', () => {
const configOptions = new ConfigOptions('.');
configOptions.defaultPythonVersion = PythonVersion.V3_8;
configOptions.defaultPythonPlatform = 'windows';
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['staticExpressions1.py'], configOptions);
TestUtils.validateResults(analysisResults1, 6);
configOptions.defaultPythonVersion = PythonVersion.V3_11;
configOptions.defaultPythonPlatform = 'Linux';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['staticExpressions1.py'], configOptions);
TestUtils.validateResults(analysisResults2, 3);
configOptions.defineConstant.set('DEFINED_TRUE', true);
configOptions.defineConstant.set('DEFINED_FALSE', false);
configOptions.defineConstant.set('DEFINED_STR', 'hi!');
const analysisResults3 = TestUtils.typeAnalyzeSampleFiles(['staticExpressions1.py'], configOptions);
TestUtils.validateResults(analysisResults3, 0);
});

View File

@ -67,6 +67,17 @@
"pattern": "^(.*)$"
}
},
"defineConstant": {
"$id": "#/properties/defineConstant",
"type": "object",
"title": "Identifiers that should be treated as constants",
"properties": {
},
"additionalProperties": {
"type": ["string", "boolean"],
"title": "Value of constant (boolean or string)"
}
},
"typeCheckingMode": {
"$id": "#/properties/typeCheckingMode",
"type": "string",