mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-11 07:55:56 +03:00
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:
parent
9b407f6c9e
commit
794d151050
@ -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 you’re 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"
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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) => {
|
||||
|
@ -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':
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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') {
|
||||
|
@ -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!"
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user