mirror of
https://github.com/microsoft/pyright.git
synced 2024-10-26 10:55:06 +03:00
Fixed a bug that resulted in a stack overflow in rare cases where an unannotated decorator was used on hundreds of functions or methods within the same file.
This commit is contained in:
parent
b9c187b9e8
commit
5ed124e090
@ -598,7 +598,15 @@ export class Binder extends ParseTreeWalker {
|
||||
this.walk(argNode);
|
||||
});
|
||||
});
|
||||
this._createCallFlowNode(node);
|
||||
|
||||
// Create a call flow node. We'll skip this if the call is part of
|
||||
// a decorator. We assume that decorators are not NoReturn functions.
|
||||
// There are libraries that make extensive use of unannotated decorators,
|
||||
// and this can lead to a performance issue when walking the control
|
||||
// flow graph if we need to evaluate every decorator.
|
||||
if (!ParseTreeUtils.isNodeContainedWithinNodeType(node, ParseNodeType.Decorator)) {
|
||||
this._createCallFlowNode(node);
|
||||
}
|
||||
|
||||
// Is this an manipulation of dunder all?
|
||||
if (
|
||||
|
@ -61,6 +61,7 @@ import {
|
||||
isOverloadedFunction,
|
||||
isTypeSame,
|
||||
isTypeVar,
|
||||
maxTypeRecursionCount,
|
||||
ModuleType,
|
||||
NeverType,
|
||||
removeIncompleteUnknownFromUnion,
|
||||
@ -116,6 +117,7 @@ export function getCodeFlowEngine(
|
||||
const callIsNoReturnCache = new Map<number, boolean>();
|
||||
const isExceptionContextManagerCache = new Map<number, boolean>();
|
||||
let flowIncompleteGeneration = 1;
|
||||
let noReturnAnalysisDepth = 0;
|
||||
|
||||
// Creates a new code flow analyzer that can be used to narrow the types
|
||||
// of the expressions within an execution context. Each code flow analyzer
|
||||
@ -1199,11 +1201,13 @@ export function getCodeFlowEngine(
|
||||
return isCompatible;
|
||||
}
|
||||
|
||||
// Determines whether a call associated with this flow node returns a NoReturn
|
||||
// type, thus preventing further traversal of the code flow graph.
|
||||
function isCallNoReturn(evaluator: TypeEvaluator, flowNode: FlowCall) {
|
||||
const node = flowNode.node;
|
||||
|
||||
if (isPrintCallNoReturnEnabled) {
|
||||
console.log(`isCallNoReturn@${flowNode.id} Pre`);
|
||||
console.log(`isCallNoReturn@${flowNode.id} Pre depth ${noReturnAnalysisDepth}`);
|
||||
}
|
||||
|
||||
// See if this information is cached already.
|
||||
@ -1217,182 +1221,197 @@ export function getCodeFlowEngine(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Initially set to false to avoid infinite recursion.
|
||||
// See if we've exceeded the max recursion depth.
|
||||
if (noReturnAnalysisDepth > maxTypeRecursionCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initially set to false to avoid recursion.
|
||||
callIsNoReturnCache.set(node.id, false);
|
||||
|
||||
let noReturnTypeCount = 0;
|
||||
let subtypeCount = 0;
|
||||
noReturnAnalysisDepth++;
|
||||
|
||||
// Evaluate the call base type.
|
||||
const callType = evaluator.getTypeOfExpression(node.leftExpression, EvaluatorFlags.DoNotSpecialize).type;
|
||||
try {
|
||||
let noReturnTypeCount = 0;
|
||||
let subtypeCount = 0;
|
||||
|
||||
doForEachSubtype(callType, (callSubtype) => {
|
||||
// Track the number of subtypes we've examined.
|
||||
subtypeCount++;
|
||||
// Evaluate the call base type.
|
||||
const callType = evaluator.getTypeOfExpression(node.leftExpression, EvaluatorFlags.DoNotSpecialize).type;
|
||||
|
||||
let functionType: FunctionType | undefined;
|
||||
if (isInstantiableClass(callSubtype)) {
|
||||
// Does the class have a custom metaclass that implements a `__call__` method?
|
||||
// If so, it will be called instead of `__init__` or `__new__`. We'll assume
|
||||
// in this case that the __call__ method is not a NoReturn type.
|
||||
if (
|
||||
callSubtype.details.effectiveMetaclass &&
|
||||
isClass(callSubtype.details.effectiveMetaclass) &&
|
||||
!ClassType.isBuiltIn(callSubtype.details.effectiveMetaclass, 'type')
|
||||
) {
|
||||
const metaclassCallMember = lookUpClassMember(
|
||||
callSubtype.details.effectiveMetaclass,
|
||||
'__call__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables | ClassMemberLookupFlags.SkipObjectBaseClass
|
||||
);
|
||||
if (metaclassCallMember) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
doForEachSubtype(callType, (callSubtype) => {
|
||||
// Track the number of subtypes we've examined.
|
||||
subtypeCount++;
|
||||
|
||||
let constructorMember = lookUpClassMember(
|
||||
callSubtype,
|
||||
'__init__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables | ClassMemberLookupFlags.SkipObjectBaseClass
|
||||
);
|
||||
|
||||
if (constructorMember === undefined) {
|
||||
constructorMember = lookUpClassMember(
|
||||
callSubtype,
|
||||
'__new__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables | ClassMemberLookupFlags.SkipObjectBaseClass
|
||||
);
|
||||
}
|
||||
|
||||
if (constructorMember) {
|
||||
const constructorType = evaluator.getTypeOfMember(constructorMember);
|
||||
if (constructorType) {
|
||||
if (isFunction(constructorType) || isOverloadedFunction(constructorType)) {
|
||||
const boundConstructorType = evaluator.bindFunctionToClassOrObject(
|
||||
undefined,
|
||||
constructorType
|
||||
);
|
||||
if (boundConstructorType) {
|
||||
callSubtype = boundConstructorType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isClassInstance(callSubtype)) {
|
||||
const callMember = lookUpClassMember(
|
||||
callSubtype,
|
||||
'__call__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables
|
||||
);
|
||||
if (callMember) {
|
||||
const callMemberType = evaluator.getTypeOfMember(callMember);
|
||||
if (callMemberType) {
|
||||
if (isFunction(callMemberType) || isOverloadedFunction(callMemberType)) {
|
||||
const boundCallType = evaluator.bindFunctionToClassOrObject(undefined, callMemberType);
|
||||
if (boundCallType) {
|
||||
callSubtype = boundCallType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isFunction(callSubtype)) {
|
||||
functionType = callSubtype;
|
||||
} else if (isOverloadedFunction(callSubtype)) {
|
||||
// Use the last overload, which should be the most general.
|
||||
const overloadedFunction = callSubtype;
|
||||
functionType = overloadedFunction.overloads[overloadedFunction.overloads.length - 1];
|
||||
}
|
||||
|
||||
if (functionType) {
|
||||
const returnType = functionType.details.declaredReturnType;
|
||||
if (FunctionType.isAsync(functionType)) {
|
||||
let functionType: FunctionType | undefined;
|
||||
if (isInstantiableClass(callSubtype)) {
|
||||
// Does the class have a custom metaclass that implements a `__call__` method?
|
||||
// If so, it will be called instead of `__init__` or `__new__`. We'll assume
|
||||
// in this case that the __call__ method is not a NoReturn type.
|
||||
if (
|
||||
returnType &&
|
||||
isClassInstance(returnType) &&
|
||||
ClassType.isBuiltIn(returnType, 'Coroutine') &&
|
||||
returnType.typeArguments &&
|
||||
returnType.typeArguments.length >= 3
|
||||
callSubtype.details.effectiveMetaclass &&
|
||||
isClass(callSubtype.details.effectiveMetaclass) &&
|
||||
!ClassType.isBuiltIn(callSubtype.details.effectiveMetaclass, 'type')
|
||||
) {
|
||||
if (isNever(returnType.typeArguments[2])) {
|
||||
if (node.parent?.nodeType === ParseNodeType.Await) {
|
||||
const metaclassCallMember = lookUpClassMember(
|
||||
callSubtype.details.effectiveMetaclass,
|
||||
'__call__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables | ClassMemberLookupFlags.SkipObjectBaseClass
|
||||
);
|
||||
if (metaclassCallMember) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let constructorMember = lookUpClassMember(
|
||||
callSubtype,
|
||||
'__init__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables | ClassMemberLookupFlags.SkipObjectBaseClass
|
||||
);
|
||||
|
||||
if (constructorMember === undefined) {
|
||||
constructorMember = lookUpClassMember(
|
||||
callSubtype,
|
||||
'__new__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables | ClassMemberLookupFlags.SkipObjectBaseClass
|
||||
);
|
||||
}
|
||||
|
||||
if (constructorMember) {
|
||||
const constructorType = evaluator.getTypeOfMember(constructorMember);
|
||||
if (constructorType) {
|
||||
if (isFunction(constructorType) || isOverloadedFunction(constructorType)) {
|
||||
const boundConstructorType = evaluator.bindFunctionToClassOrObject(
|
||||
undefined,
|
||||
constructorType
|
||||
);
|
||||
if (boundConstructorType) {
|
||||
callSubtype = boundConstructorType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isClassInstance(callSubtype)) {
|
||||
const callMember = lookUpClassMember(
|
||||
callSubtype,
|
||||
'__call__',
|
||||
ClassMemberLookupFlags.SkipInstanceVariables
|
||||
);
|
||||
if (callMember) {
|
||||
const callMemberType = evaluator.getTypeOfMember(callMember);
|
||||
if (callMemberType) {
|
||||
if (isFunction(callMemberType) || isOverloadedFunction(callMemberType)) {
|
||||
const boundCallType = evaluator.bindFunctionToClassOrObject(undefined, callMemberType);
|
||||
if (boundCallType) {
|
||||
callSubtype = boundCallType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isFunction(callSubtype)) {
|
||||
functionType = callSubtype;
|
||||
} else if (isOverloadedFunction(callSubtype)) {
|
||||
// Use the last overload, which should be the most general.
|
||||
const overloadedFunction = callSubtype;
|
||||
functionType = overloadedFunction.overloads[overloadedFunction.overloads.length - 1];
|
||||
}
|
||||
|
||||
if (functionType) {
|
||||
const returnType = functionType.details.declaredReturnType;
|
||||
if (FunctionType.isAsync(functionType)) {
|
||||
if (
|
||||
returnType &&
|
||||
isClassInstance(returnType) &&
|
||||
ClassType.isBuiltIn(returnType, 'Coroutine') &&
|
||||
returnType.typeArguments &&
|
||||
returnType.typeArguments.length >= 3
|
||||
) {
|
||||
if (isNever(returnType.typeArguments[2])) {
|
||||
if (node.parent?.nodeType === ParseNodeType.Await) {
|
||||
noReturnTypeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (returnType) {
|
||||
if (isNever(returnType)) {
|
||||
noReturnTypeCount++;
|
||||
}
|
||||
} else if (functionType.details.declaration) {
|
||||
// If the function has yield expressions, it's a generator, and
|
||||
// we'll assume the yield statements are reachable. Also, don't
|
||||
// infer a "no return" type for abstract methods.
|
||||
if (
|
||||
!functionType.details.declaration.yieldStatements &&
|
||||
!FunctionType.isAbstractMethod(functionType) &&
|
||||
!FunctionType.isStubDefinition(functionType) &&
|
||||
!FunctionType.isPyTypedDefinition(functionType)
|
||||
) {
|
||||
// Check specifically for a common idiom where the only statement
|
||||
// (other than a possible docstring) is a "raise NotImplementedError".
|
||||
const functionStatements = functionType.details.declaration.node.suite.statements;
|
||||
|
||||
let foundRaiseNotImplemented = false;
|
||||
for (const statement of functionStatements) {
|
||||
if (
|
||||
statement.nodeType !== ParseNodeType.StatementList ||
|
||||
statement.statements.length !== 1
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
const simpleStatement = statement.statements[0];
|
||||
if (simpleStatement.nodeType === ParseNodeType.StringList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
simpleStatement.nodeType === ParseNodeType.Raise &&
|
||||
simpleStatement.typeExpression
|
||||
) {
|
||||
// Check for "raise NotImplementedError" or "raise NotImplementedError()"
|
||||
const isNotImplementedName = (node: ParseNode) => {
|
||||
return (
|
||||
node?.nodeType === ParseNodeType.Name &&
|
||||
node.value === 'NotImplementedError'
|
||||
);
|
||||
};
|
||||
|
||||
if (isNotImplementedName(simpleStatement.typeExpression)) {
|
||||
foundRaiseNotImplemented = true;
|
||||
} else if (
|
||||
simpleStatement.typeExpression.nodeType === ParseNodeType.Call &&
|
||||
isNotImplementedName(simpleStatement.typeExpression.leftExpression)
|
||||
) {
|
||||
foundRaiseNotImplemented = true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!foundRaiseNotImplemented && !isAfterNodeReachable(evaluator, functionType)) {
|
||||
noReturnTypeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (returnType) {
|
||||
if (isNever(returnType)) {
|
||||
noReturnTypeCount++;
|
||||
}
|
||||
} else if (functionType.details.declaration) {
|
||||
// If the function has yield expressions, it's a generator, and
|
||||
// we'll assume the yield statements are reachable. Also, don't
|
||||
// infer a "no return" type for abstract methods.
|
||||
if (
|
||||
!functionType.details.declaration.yieldStatements &&
|
||||
!FunctionType.isAbstractMethod(functionType) &&
|
||||
!FunctionType.isStubDefinition(functionType) &&
|
||||
!FunctionType.isPyTypedDefinition(functionType)
|
||||
) {
|
||||
// Check specifically for a common idiom where the only statement
|
||||
// (other than a possible docstring) is a "raise NotImplementedError".
|
||||
const functionStatements = functionType.details.declaration.node.suite.statements;
|
||||
|
||||
let foundRaiseNotImplemented = false;
|
||||
for (const statement of functionStatements) {
|
||||
if (
|
||||
statement.nodeType !== ParseNodeType.StatementList ||
|
||||
statement.statements.length !== 1
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
const simpleStatement = statement.statements[0];
|
||||
if (simpleStatement.nodeType === ParseNodeType.StringList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (simpleStatement.nodeType === ParseNodeType.Raise && simpleStatement.typeExpression) {
|
||||
// Check for "raise NotImplementedError" or "raise NotImplementedError()"
|
||||
const isNotImplementedName = (node: ParseNode) => {
|
||||
return (
|
||||
node?.nodeType === ParseNodeType.Name && node.value === 'NotImplementedError'
|
||||
);
|
||||
};
|
||||
|
||||
if (isNotImplementedName(simpleStatement.typeExpression)) {
|
||||
foundRaiseNotImplemented = true;
|
||||
} else if (
|
||||
simpleStatement.typeExpression.nodeType === ParseNodeType.Call &&
|
||||
isNotImplementedName(simpleStatement.typeExpression.leftExpression)
|
||||
) {
|
||||
foundRaiseNotImplemented = true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!foundRaiseNotImplemented && !isAfterNodeReachable(evaluator, functionType)) {
|
||||
noReturnTypeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The call is considered NoReturn if all subtypes evaluate to NoReturn.
|
||||
const callIsNoReturn = subtypeCount > 0 && noReturnTypeCount === subtypeCount;
|
||||
|
||||
// Cache the value for next time.
|
||||
callIsNoReturnCache.set(node.id, callIsNoReturn);
|
||||
|
||||
if (isPrintCallNoReturnEnabled) {
|
||||
console.log(`isCallNoReturn@${flowNode.id} Post: ${callIsNoReturn ? 'true' : 'false'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// The call is considered NoReturn if all subtypes evaluate to NoReturn.
|
||||
const callIsNoReturn = subtypeCount > 0 && noReturnTypeCount === subtypeCount;
|
||||
|
||||
// Cache the value for next time.
|
||||
callIsNoReturnCache.set(node.id, callIsNoReturn);
|
||||
|
||||
if (isPrintCallNoReturnEnabled) {
|
||||
console.log(`isCallNoReturn@${flowNode.id} Post: ${callIsNoReturn ? 'true' : 'false'}`);
|
||||
return callIsNoReturn;
|
||||
} finally {
|
||||
noReturnAnalysisDepth--;
|
||||
}
|
||||
|
||||
return callIsNoReturn;
|
||||
}
|
||||
|
||||
function isAfterNodeReachable(evaluator: TypeEvaluator, functionType: FunctionType) {
|
||||
|
Loading…
Reference in New Issue
Block a user