Implement Symbol-based disposability checking

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-07-24 16:45:54 +00:00
parent 6ff8065d1c
commit dadd7418ed
2 changed files with 82 additions and 19 deletions

View File

@ -17546,10 +17546,47 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return (deferredGlobalDisposableType ||= getGlobalType("Disposable" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType;
}
function getGlobalAsyncDisposableType(reportErrors: boolean) {
return (deferredGlobalAsyncDisposableType ||= getGlobalType("AsyncDisposable" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType;
}
function getGlobalAsyncDisposableType(reportErrors: boolean) {
return (deferredGlobalAsyncDisposableType ||= getGlobalType("AsyncDisposable" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType;
}
function checkTypeIsDisposable(type: Type): boolean {
if (type.flags & (TypeFlags.Null | TypeFlags.Undefined)) {
return true; // null and undefined are allowed
}
// Handle union types
if (type.flags & TypeFlags.Union) {
return every((type as UnionType).types, checkTypeIsDisposable);
}
const disposePropertyName = getPropertyNameForKnownSymbolName("dispose");
const disposeProperty = getPropertyOfType(type, disposePropertyName);
return !!disposeProperty && !(disposeProperty.flags & SymbolFlags.Optional);
}
function checkTypeIsAsyncDisposable(type: Type): boolean {
if (type.flags & (TypeFlags.Null | TypeFlags.Undefined)) {
return true; // null and undefined are allowed
}
// Handle union types
if (type.flags & TypeFlags.Union) {
return every((type as UnionType).types, checkTypeIsAsyncDisposable);
}
const asyncDisposePropertyName = getPropertyNameForKnownSymbolName("asyncDispose");
const asyncDisposeProperty = getPropertyOfType(type, asyncDisposePropertyName);
if (asyncDisposeProperty && !(asyncDisposeProperty.flags & SymbolFlags.Optional)) {
return true;
}
// For await using, also check for Symbol.dispose as a fallback
return checkTypeIsDisposable(type);
}
function getGlobalTypeOrUndefined(name: __String, arity = 0): ObjectType | undefined {
const symbol = getGlobalSymbol(name, SymbolFlags.Type, /*diagnostic*/ undefined);
return symbol && getTypeOfGlobalSymbol(symbol, arity) as GenericType;
@ -44997,21 +45034,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!isJSObjectLiteralInitializer && node.parent.parent.kind !== SyntaxKind.ForInStatement) {
const initializerType = checkExpressionCached(initializer);
checkTypeAssignableToAndOptionallyElaborate(initializerType, type, node, initializer, /*headMessage*/ undefined);
const blockScopeKind = getCombinedNodeFlagsCached(node) & NodeFlags.BlockScoped;
if (blockScopeKind === NodeFlags.AwaitUsing) {
const globalAsyncDisposableType = getGlobalAsyncDisposableType(/*reportErrors*/ true);
const globalDisposableType = getGlobalDisposableType(/*reportErrors*/ true);
if (globalAsyncDisposableType !== emptyObjectType && globalDisposableType !== emptyObjectType) {
const optionalDisposableType = getUnionType([globalAsyncDisposableType, globalDisposableType, nullType, undefinedType]);
checkTypeAssignableTo(widenTypeForVariableLikeDeclaration(initializerType, node), optionalDisposableType, initializer, Diagnostics.The_initializer_of_an_await_using_declaration_must_be_either_an_object_with_a_Symbol_asyncDispose_or_Symbol_dispose_method_or_be_null_or_undefined);
}
}
else if (blockScopeKind === NodeFlags.Using) {
const globalDisposableType = getGlobalDisposableType(/*reportErrors*/ true);
if (globalDisposableType !== emptyObjectType) {
const optionalDisposableType = getUnionType([globalDisposableType, nullType, undefinedType]);
checkTypeAssignableTo(widenTypeForVariableLikeDeclaration(initializerType, node), optionalDisposableType, initializer, Diagnostics.The_initializer_of_a_using_declaration_must_be_either_an_object_with_a_Symbol_dispose_method_or_be_null_or_undefined);
}
const blockScopeKind = getCombinedNodeFlagsCached(node) & NodeFlags.BlockScoped;
if (blockScopeKind === NodeFlags.AwaitUsing) {
if (!checkTypeIsAsyncDisposable(widenTypeForVariableLikeDeclaration(initializerType, node))) {
error(initializer, Diagnostics.The_initializer_of_an_await_using_declaration_must_be_either_an_object_with_a_Symbol_asyncDispose_or_Symbol_dispose_method_or_be_null_or_undefined);
}
}
else if (blockScopeKind === NodeFlags.Using) {
if (!checkTypeIsDisposable(widenTypeForVariableLikeDeclaration(initializerType, node))) {
error(initializer, Diagnostics.The_initializer_of_a_using_declaration_must_be_either_an_object_with_a_Symbol_dispose_method_or_be_null_or_undefined);
}
}
}
}

View File

@ -0,0 +1,31 @@
// @target: esnext
// @lib: esnext
// Test case that demonstrates the issue from https://github.com/microsoft/TypeScript/issues/62121
// When an empty global Disposable interface is declared, it should NOT affect
// the checking for Symbol.dispose properties
declare global {
interface Disposable {}
}
// This should pass - has Symbol.dispose method
const validDisposable = {
[Symbol.dispose]() {
// disposed
}
};
// This should fail - no Symbol.dispose method
const invalidDisposable = {
cleanup() {
// cleanup
}
};
// With the fix, the checker should directly check for Symbol.dispose properties
// rather than relying on assignability to the global Disposable interface
using valid = validDisposable; // should pass
using invalid = invalidDisposable; // should error
export {};