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:
Eric Traut 2022-05-05 10:22:33 -07:00
parent b9c187b9e8
commit 5ed124e090
2 changed files with 189 additions and 162 deletions

View File

@ -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 (

View File

@ -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) {