From 4839b8481ecf773d51ad9a6247d40a47ed94f962 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 11 Jun 2025 20:59:59 -0700 Subject: [PATCH 1/4] Extensions: bind unconditionally of LangVer --- .../CSharp/Portable/Binder/Binder.cs | 13 + .../Portable/Binder/Binder_Conversions.cs | 93 +----- .../Portable/Binder/Binder_Expressions.cs | 1 + .../Portable/Binder/Binder_Invocation.cs | 1 + .../CSharp/Portable/Binder/Binder_Lookup.cs | 41 ++- .../Portable/Binder/Binder_Operators.cs | 2 + .../Portable/Binder/ForEachLoopBinder.cs | 4 + .../CSharp/Portable/LanguageVersion.cs | 5 - .../Test/Emit3/Semantics/ExtensionTests.cs | 292 ++++++++++++++---- 9 files changed, 288 insertions(+), 164 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.cs b/src/Compilers/CSharp/Portable/Binder/Binder.cs index 4411b2b1e45ea..bec8a6f2ae4ae 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.cs @@ -749,6 +749,19 @@ internal static void ReportDiagnosticsIfObsoleteInternal(BindingDiagnosticBag di } } + internal static bool IsDisallowedExtensionInOlderLangVer(MethodSymbol symbol) + { + return symbol.GetIsNewExtensionMember() && symbol.IsStatic; + } + + internal static void ReportDiagnosticsIfDisallowedExtension(BindingDiagnosticBag diagnostics, MethodSymbol method, SyntaxNode syntax) + { + if (IsDisallowedExtensionInOlderLangVer(method)) + { + MessageID.IDS_FeatureExtensions.CheckFeatureAvailability(diagnostics, syntax); + } + } + internal static void ReportDiagnosticsIfUnmanagedCallersOnly(BindingDiagnosticBag diagnostics, MethodSymbol symbol, SyntaxNodeOrToken syntax, bool isDelegateConversion) { var unmanagedCallersOnlyAttributeData = symbol.GetUnmanagedCallersOnlyAttributeData(forceComplete: false); diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs index 3cb962361b12a..b4536e1a4916c 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs @@ -1069,6 +1069,7 @@ private bool HasCollectionInitializerTypeInProgress(SyntaxNode syntax, TypeSymbo ReportDiagnosticsIfObsolete(diagnostics, collectionBuilderMethod.ContainingType, syntax, hasBaseReceiver: false); ReportDiagnosticsIfObsolete(diagnostics, collectionBuilderMethod, syntax, hasBaseReceiver: false); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, collectionBuilderMethod, syntax, isDelegateConversion: false); + Debug.Assert(!collectionBuilderMethod.GetIsNewExtensionMember()); return collectionBuilderMethod; } @@ -1430,15 +1431,21 @@ static bool bindMethodGroupInvocation( { addMethodBinder.ReportDiagnosticsIfObsolete(diagnostics, addMethods[0], syntax, hasBaseReceiver: false); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, addMethods[0], syntax, isDelegateConversion: false); + Debug.Assert(!IsDisallowedExtensionInOlderLangVer(addMethods[0])); } } } else { - result = bindInvocationExpressionContinued( - addMethodBinder, syntax, expression, resolution.OverloadResolutionResult, resolution.AnalyzedArguments, - resolution.MethodGroup, diagnostics: diagnostics, out var addMethod); - addMethods = addMethod is null ? [] : [addMethod]; + Debug.Assert(!resolution.OverloadResolutionResult.Succeeded); + + resolution.OverloadResolutionResult.ReportDiagnostics( + binder: addMethodBinder, location: GetLocationForOverloadResolutionDiagnostic(syntax, expression), nodeOpt: syntax, diagnostics: diagnostics, name: WellKnownMemberNames.CollectionInitializerAddMethodName, + receiver: resolution.MethodGroup.Receiver, invokedExpression: expression, arguments: analyzedArguments, memberGroup: resolution.MethodGroup.Methods.ToImmutable(), + typeContainingConstructor: null, delegateTypeBeingInvoked: null, queryClause: null); + + addMethods = []; + result = false; } } } @@ -1554,83 +1561,6 @@ static ImmutableArray filterOutBadGenericMethods( return resultBuilder.ToImmutableAndFree(); } - - // This is what BindInvocationExpressionContinued is doing in terms of reporting diagnostics and detecting a failure - static bool bindInvocationExpressionContinued( - Binder addMethodBinder, - SyntaxNode node, - SyntaxNode expression, - OverloadResolutionResult result, - AnalyzedArguments analyzedArguments, - MethodGroup methodGroup, - BindingDiagnosticBag diagnostics, - out MethodSymbol? addMethod) - { - Debug.Assert(node != null); - Debug.Assert(methodGroup != null); - Debug.Assert(methodGroup.Error == null); - Debug.Assert(methodGroup.Methods.Count > 0); - - var invokedAsExtensionMethod = methodGroup.IsExtensionMethodGroup; - - // We have already determined that we are not in a situation where we can successfully do - // a dynamic binding. We might be in one of the following situations: - // - // * There were dynamic arguments but overload resolution still found zero applicable candidates. - // * There were no dynamic arguments and overload resolution found zero applicable candidates. - // * There were no dynamic arguments and overload resolution found multiple applicable candidates - // without being able to find the best one. - // - // In those three situations we might give an additional error. - - if (!result.Succeeded) - { - // Since there were no argument errors to report, we report an error on the invocation itself. - result.ReportDiagnostics( - binder: addMethodBinder, location: GetLocationForOverloadResolutionDiagnostic(node, expression), nodeOpt: node, diagnostics: diagnostics, name: WellKnownMemberNames.CollectionInitializerAddMethodName, - receiver: methodGroup.Receiver, invokedExpression: expression, arguments: analyzedArguments, memberGroup: methodGroup.Methods.ToImmutable(), - typeContainingConstructor: null, delegateTypeBeingInvoked: null, queryClause: null); - - addMethod = null; - return false; - } - - // Otherwise, there were no dynamic arguments and overload resolution found a unique best candidate. - // We still have to determine if it passes final validation. - - var methodResult = result.ValidResult; - var method = methodResult.Member; - - // Tracked by https://github.com/dotnet/roslyn/issues/76130: It looks like we added a bunch of code in BindInvocationExpressionContinued at this position - // that specifically deals with new extension methods. It adjusts analyzedArguments, etc. - // It is very likely we need to do the same here. - - // It is possible that overload resolution succeeded, but we have chosen an - // instance method and we're in a static method. A careful reading of the - // overload resolution spec shows that the "final validation" stage allows an - // "implicit this" on any method call, not just method calls from inside - // instance methods. Therefore we must detect this scenario here, rather than in - // overload resolution. - - var receiver = methodGroup.Receiver; - - // Note: we specifically want to do final validation (7.6.5.1) without checking delegate compatibility (15.2), - // so we're calling MethodGroupFinalValidation directly, rather than via MethodGroupConversionHasErrors. - // Note: final validation wants the receiver that corresponds to the source representation - // (i.e. the first argument, if invokedAsExtensionMethod). - var gotError = addMethodBinder.MemberGroupFinalValidation(receiver, method, expression, diagnostics, invokedAsExtensionMethod); - - addMethodBinder.ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); - ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); - - // No use site errors, but there could be use site warnings. - // If there are any use site warnings, they have already been reported by overload resolution. - Debug.Assert(!method.HasUseSiteError, "Shouldn't have reached this point if there were use site errors."); - Debug.Assert(!method.IsRuntimeFinalizer()); - - addMethod = method; - return !gotError; - } } /// @@ -3051,6 +2981,7 @@ private bool MethodGroupConversionHasErrors( ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, selectedMethod, syntax, isDelegateConversion: true); } ReportDiagnosticsIfObsolete(diagnostics, selectedMethod, syntax, hasBaseReceiver: false); + ReportDiagnosticsIfDisallowedExtension(diagnostics, selectedMethod, syntax); // No use site errors, but there could be use site warnings. // If there are use site warnings, they were reported during the overload resolution process diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs index 301cbb675dc7c..f6859794d4b95 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs @@ -8030,6 +8030,7 @@ private BoundExpression MakeMemberAccessValue(BoundExpression expr, BindingDiagn private BoundExpression GetExtensionMemberAccess(SyntaxNode syntax, BoundExpression? receiver, Symbol extensionMember, BindingDiagnosticBag diagnostics) { + MessageID.IDS_FeatureExtensions.CheckFeatureAvailability(diagnostics, syntax); receiver = ReplaceTypeOrValueReceiver(receiver, useType: extensionMember.IsStatic, diagnostics); switch (extensionMember) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs index 41b580dc6a935..da2053e8b56ea 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs @@ -1325,6 +1325,7 @@ private BoundCall BindInvocationExpressionContinued( ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); + ReportDiagnosticsIfDisallowedExtension(diagnostics, method, node); // No use site errors, but there could be use site warnings. // If there are any use site warnings, they have already been reported by overload resolution. diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs index d5ac10f288222..e85e16ee1e047 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs @@ -202,37 +202,34 @@ internal void EnumerateAllExtensionMembersInSingleBinder(ArrayBuilder? implementationsToShadow = null; // 1. Collect new extension members - if (this.Compilation.LanguageVersion.AllowNewExtensions()) - { - var extensionDeclarations = ArrayBuilder.GetInstance(); - this.GetExtensionDeclarations(extensionDeclarations, originalBinder); + var extensionDeclarations = ArrayBuilder.GetInstance(); + this.GetExtensionDeclarations(extensionDeclarations, originalBinder); - foreach (NamedTypeSymbol extensionDeclaration in extensionDeclarations) + foreach (NamedTypeSymbol extensionDeclaration in extensionDeclarations) + { + if (extensionDeclaration.ExtensionParameter is null) { - if (extensionDeclaration.ExtensionParameter is null) - { - continue; - } + continue; + } - var candidates = name is null ? extensionDeclaration.GetMembers() : extensionDeclaration.GetMembers(name); + var candidates = name is null ? extensionDeclaration.GetMembers() : extensionDeclaration.GetMembers(name); - foreach (var candidate in candidates) - { - SingleLookupResult resultOfThisMember = originalBinder.CheckViability(candidate, arity, options, null, diagnose: true, useSiteInfo: ref useSiteInfo); - result.Add(resultOfThisMember); + foreach (var candidate in candidates) + { + SingleLookupResult resultOfThisMember = originalBinder.CheckViability(candidate, arity, options, null, diagnose: true, useSiteInfo: ref useSiteInfo); + result.Add(resultOfThisMember); - if (candidate is MethodSymbol { IsStatic: false } shadows && - shadows.OriginalDefinition.TryGetCorrespondingExtensionImplementationMethod() is { } toShadow) - { - implementationsToShadow ??= PooledHashSet.GetInstance(); - implementationsToShadow.Add(toShadow); - } + if (candidate is MethodSymbol { IsStatic: false } shadows && + shadows.OriginalDefinition.TryGetCorrespondingExtensionImplementationMethod() is { } toShadow) + { + implementationsToShadow ??= PooledHashSet.GetInstance(); + implementationsToShadow.Add(toShadow); } } - - extensionDeclarations.Free(); } + extensionDeclarations.Free(); + // 2. Collect classic extension methods var extensionMethods = ArrayBuilder.GetInstance(); this.GetCandidateExtensionMethods(extensionMethods, name, arity, options, originalBinder: originalBinder); diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs index 1e81912b14d95..78fdf8c093bfa 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs @@ -334,6 +334,7 @@ private BoundExpression BindCompoundAssignment(AssignmentExpressionSyntax node, ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); + Debug.Assert(!IsDisallowedExtensionInOlderLangVer(method)); inPlaceResult = new BoundCompoundAssignmentOperator( node, @@ -2647,6 +2648,7 @@ private BoundExpression BindIncrementOperator(ExpressionSyntax node, ExpressionS ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); + Debug.Assert(!IsDisallowedExtensionInOlderLangVer(method)); inPlaceResult = new BoundIncrementOperator( node, diff --git a/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs b/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs index 971adeea4bcae..3d8da7b955392 100644 --- a/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs @@ -465,12 +465,16 @@ private BoundForEachStatement BindForEachPartsWorker(BindingDiagnosticBag diagno var foreachKeyword = _syntax.ForEachKeyword; ReportDiagnosticsIfObsolete(diagnostics, getEnumeratorMethod, foreachKeyword, hasBaseReceiver: false); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, getEnumeratorMethod, foreachKeyword, isDelegateConversion: false); + Debug.Assert(!IsDisallowedExtensionInOlderLangVer(getEnumeratorMethod)); + // MoveNext is an instance method, so it does not need to have unmanaged callers only diagnostics reported. // Either a diagnostic was reported at the declaration of the method (for the invalid attribute), or MoveNext // is marked as not supported and we won't get here in the first place (for metadata import). ReportDiagnosticsIfObsolete(diagnostics, builder.MoveNextInfo.Method, foreachKeyword, hasBaseReceiver: false); ReportDiagnosticsIfObsolete(diagnostics, builder.CurrentPropertyGetter, foreachKeyword, hasBaseReceiver: false); ReportDiagnosticsIfObsolete(diagnostics, builder.CurrentPropertyGetter.AssociatedSymbol, foreachKeyword, hasBaseReceiver: false); + Debug.Assert(!IsDisallowedExtensionInOlderLangVer(builder.MoveNextInfo.Method)); + Debug.Assert(!IsDisallowedExtensionInOlderLangVer(builder.CurrentPropertyGetter)); // We want to convert from inferredType in the array/string case and builder.ElementType in the enumerator case, // but it turns out that these are equivalent (when both are available). diff --git a/src/Compilers/CSharp/Portable/LanguageVersion.cs b/src/Compilers/CSharp/Portable/LanguageVersion.cs index ec5d56afa9904..4d429582030ef 100644 --- a/src/Compilers/CSharp/Portable/LanguageVersion.cs +++ b/src/Compilers/CSharp/Portable/LanguageVersion.cs @@ -572,10 +572,5 @@ internal static bool AllowImprovedOverloadCandidates(this LanguageVersion self) { return self >= MessageID.IDS_FeatureImprovedOverloadCandidates.RequiredVersion(); } - - internal static bool AllowNewExtensions(this LanguageVersion self) - { - return self >= MessageID.IDS_FeatureExtensions.RequiredVersion(); - } } } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs index 5424ae1691dc0..2a804aa871457 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs @@ -16032,7 +16032,7 @@ static class E { extension(MyCollection c) { - public void Add(ref int i) { System.Console.Write("ran"); } + public void Add(ref int i) { } } } @@ -20948,6 +20948,17 @@ static class E [Fact] public void ExtensionMemberLookup_PatternBased_PositionalPattern() { + var libSrc = """ +public static class E +{ + extension(T t) + { + public void Deconstruct(out int i, out int j) { i = 42; j = 43; } + } +} +"""; + var libRef = CreateCompilation(libSrc).EmitToImageReference(); + var src = """ var c = new C(); if (c is var (x, y)) @@ -20955,16 +20966,15 @@ public void ExtensionMemberLookup_PatternBased_PositionalPattern() class C { } -static class E -{ - extension(T t) - { - public void Deconstruct(out int i, out int j) { i = 42; j = 43; } - } -} """; - var comp = CreateCompilation(src); + var comp = CreateCompilation(src, references: [libRef]); CompileAndVerify(comp, expectedOutput: "(42, 43)").VerifyDiagnostics(); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics(); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); } [Fact] @@ -22555,13 +22565,10 @@ class C [Fact] public void ExtensionMemberLookup_Patterns() { - var src = """ -var c = new C(); -_ = c is { Property: 42 }; - -class C { } + var libSrc = """ +public class C { } -static class E +public static class E { extension(C c) { @@ -22572,13 +22579,28 @@ public int Property } } """; - var comp = CreateCompilation(src); + var libRef = CreateCompilation(libSrc).EmitToImageReference(); + + var src = """ +var c = new C(); +_ = c is { Property: 42 }; +"""; + var comp = CreateCompilation(src, references: [libRef]); CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); var nameColon = GetSyntax(tree, "Property:"); Assert.Equal("System.Int32 E.<>E__0.Property { get; }", model.GetSymbolInfo(nameColon.Name).Symbol.ToTestDisplayString()); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics( + // (2,12): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = c is { Property: 42 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "Property").WithArguments("extensions").WithLocation(2, 12)); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); } [Fact] @@ -23114,12 +23136,10 @@ public int Length [Fact] public void ExtensionMemberLookup_ObjectInitializer() { - var src = """ -_ = new C() { Property = 42 }; - -class C { } + var libSrc = """ +public class C { } -static class E +public static class E { extension(C c) { @@ -23127,14 +23147,28 @@ static class E } } """; + var libRef = CreateCompilation(libSrc).EmitToImageReference(); - var comp = CreateCompilation(src); + var src = """ +_ = new C() { Property = 42 }; +"""; + + var comp = CreateCompilation(src, references: [libRef]); CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); var assignment = GetSyntax(tree, "Property = 42"); Assert.Equal("System.Int32 E.<>E__0.Property { set; }", model.GetSymbolInfo(assignment.Left).Symbol.ToTestDisplayString()); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics( + // (1,15): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = new C() { Property = 42 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "Property").WithArguments("extensions").WithLocation(1, 15)); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); } [Fact] @@ -23256,12 +23290,10 @@ public int Property [Fact] public void ExtensionMemberLookup_With() { - var src = """ -_ = new S() with { Property = 42 }; - -struct S { } + var libSrc = """ +public struct S { } -static class E +public static class E { extension(S s) { @@ -23269,14 +23301,28 @@ static class E } } """; + var libRef = CreateCompilation(libSrc).EmitToImageReference(); - var comp = CreateCompilation(src); + var src = """ +_ = new S() with { Property = 42 }; +"""; + + var comp = CreateCompilation(src, references: [libRef]); CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); var assignment = GetSyntax(tree, "Property = 42"); Assert.Equal("System.Int32 E.<>E__0.Property { set; }", model.GetSymbolInfo(assignment.Left).Symbol.ToTestDisplayString()); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics( + // (1,20): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = new S() with { Property = 42 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "Property").WithArguments("extensions").WithLocation(1, 20)); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); } [Fact] @@ -33005,7 +33051,8 @@ public static class E { public void M() { } public static void M2() { } - public static int P => 0; + public int P => 0; + public static int P2 => 0; } } @@ -33020,8 +33067,10 @@ public static void M2() { } var x = new object().M; E.M(new object()); -E.get_P(); E.M2(); + +E.get_P(new object()); +E.get_P2(); """; var comp = CreateCompilation(srcCompat, references: [libRef], parseOptions: TestOptions.Regular13); comp.VerifyEmitDiagnostics(); @@ -33037,52 +33086,183 @@ public static void M2() { } System.Action a = object.M2; var x = object.M2; -_ = object.P; +_ = object.P2; """; comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); comp.VerifyEmitDiagnostics( - // (1,8): error CS0117: 'object' does not contain a definition for 'M2' + // (1,1): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. // object.M2(); - Diagnostic(ErrorCode.ERR_NoSuchMember, "M2").WithArguments("object", "M2").WithLocation(1, 8), - // (2,26): error CS0117: 'object' does not contain a definition for 'M2' + Diagnostic(ErrorCode.ERR_FeatureInPreview, "object.M2()").WithArguments("extensions").WithLocation(1, 1), + // (2,19): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. // System.Action a = object.M2; - Diagnostic(ErrorCode.ERR_NoSuchMember, "M2").WithArguments("object", "M2").WithLocation(2, 26), - // (3,16): error CS0117: 'object' does not contain a definition for 'M2' + Diagnostic(ErrorCode.ERR_FeatureInPreview, "object.M2").WithArguments("extensions").WithLocation(2, 19), + // (3,9): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. // var x = object.M2; - Diagnostic(ErrorCode.ERR_NoSuchMember, "M2").WithArguments("object", "M2").WithLocation(3, 16), - // (5,12): error CS0117: 'object' does not contain a definition for 'P' - // _ = object.P; - Diagnostic(ErrorCode.ERR_NoSuchMember, "P").WithArguments("object", "P").WithLocation(5, 12)); - verifySymbolInfo(comp, newLangVer: false); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "object.M2").WithArguments("extensions").WithLocation(3, 9), + // (5,5): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = object.P2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "object.P2").WithArguments("extensions").WithLocation(5, 5)); + verifySymbolInfo(comp); comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); comp.VerifyEmitDiagnostics(); - verifySymbolInfo(comp, newLangVer: true); + verifySymbolInfo(comp); comp = CreateCompilation(src, references: [libRef]); comp.VerifyEmitDiagnostics(); - verifySymbolInfo(comp, newLangVer: true); + verifySymbolInfo(comp); - static void verifySymbolInfo(CSharpCompilation comp, bool newLangVer) + src = """ +_ = new object().P; +"""; + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics( + // (1,5): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = new object().P; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "new object().P").WithArguments("extensions").WithLocation(1, 5)); + verifySymbolInfo(comp); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); + verifySymbolInfo(comp); + + comp = CreateCompilation(src, references: [libRef]); + comp.VerifyEmitDiagnostics(); + verifySymbolInfo(comp); + + static void verifySymbolInfo(CSharpCompilation comp) { var tree = comp.SyntaxTrees.Single(); var model = comp.GetSemanticModel(tree); var o = ((Compilation)comp).GetSpecialType(SpecialType.System_Object); - if (newLangVer) - { - AssertEqualAndNoDuplicates(["void E.<>E__0.M()"], model.LookupSymbols(position: 0, o, name: "M", includeReducedExtensionMethods: true).ToTestDisplayStrings()); - AssertEqualAndNoDuplicates(["void E.<>E__0.M2()"], model.LookupSymbols(position: 0, o, name: "M2", includeReducedExtensionMethods: true).ToTestDisplayStrings()); - AssertEqualAndNoDuplicates(["System.Int32 E.<>E__0.P { get; }"], model.LookupSymbols(position: 0, o, name: "P", includeReducedExtensionMethods: true).ToTestDisplayStrings()); - } - else - { - AssertEqualAndNoDuplicates(["void System.Object.M()"], model.LookupSymbols(position: 0, o, name: "M", includeReducedExtensionMethods: true).ToTestDisplayStrings()); - AssertEqualAndNoDuplicates([], model.LookupSymbols(position: 0, o, name: "M2", includeReducedExtensionMethods: true).ToTestDisplayStrings()); - AssertEqualAndNoDuplicates([], model.LookupSymbols(position: 0, o, name: "P", includeReducedExtensionMethods: true).ToTestDisplayStrings()); - } + AssertEqualAndNoDuplicates(["void E.<>E__0.M()"], model.LookupSymbols(position: 0, o, name: "M", includeReducedExtensionMethods: true).ToTestDisplayStrings()); + AssertEqualAndNoDuplicates(["void E.<>E__0.M2()"], model.LookupSymbols(position: 0, o, name: "M2", includeReducedExtensionMethods: true).ToTestDisplayStrings()); + AssertEqualAndNoDuplicates(["System.Int32 E.<>E__0.P { get; }"], model.LookupSymbols(position: 0, o, name: "P", includeReducedExtensionMethods: true).ToTestDisplayStrings()); + AssertEqualAndNoDuplicates(["System.Int32 E.<>E__0.P2 { get; }"], model.LookupSymbols(position: 0, o, name: "P2", includeReducedExtensionMethods: true).ToTestDisplayStrings()); + } + } + + [Fact] + public void LangVer_02() + { + var libSrc = """ +public static class E +{ + extension(object o) + { + public System.Collections.IEnumerator GetEnumerator() => throw null; + } +} + +"""; + var libComp = CreateCompilation(libSrc, parseOptions: TestOptions.RegularNext); + libComp.VerifyEmitDiagnostics(); + var libRef = libComp.EmitToImageReference(); + + var src = """ +foreach (var x in new object()) +{ +} +"""; + var comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics(); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); + + comp = CreateCompilation(src, references: [libRef]); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void LangVer_03() + { + var libSrc = """ +public static class E +{ + extension(object o) + { + public void Add(object o2) { } + } +} +"""; + var libComp = CreateCompilation(libSrc, parseOptions: TestOptions.RegularNext); + libComp.VerifyEmitDiagnostics(); + var libRef = libComp.EmitToImageReference(); + + var src = """ +using System.Collections; +using System.Collections.Generic; + +MyCollection c = [new object()]; + +public class MyCollection : IEnumerable +{ + IEnumerator IEnumerable.GetEnumerator() => throw null!; + IEnumerator IEnumerable.GetEnumerator() => throw null!; +} +"""; + var comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics(); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); + + comp = CreateCompilation(src, references: [libRef]); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void LangVer_04() + { + var libSrc = """ +namespace N; + +public static class E +{ + extension(int i) + { + public System.Action Member => () => System.Console.Write("property"); + } +} +"""; + var libComp = CreateCompilation(libSrc, parseOptions: TestOptions.RegularNext); + libComp.VerifyEmitDiagnostics(); + var libRef = libComp.EmitToImageReference(); + + var src = """ +namespace N +{ + class D + { + public static void Main() + { + 42.Member(); } } +} + +static class Classic +{ + public static void Member(this int i) { System.Console.Write("classic"); } +} +"""; + var comp = CreateCompilation(src, options: TestOptions.DebugExe); + CompileAndVerify(comp, expectedOutput: "classic").VerifyDiagnostics(); + + comp = CreateCompilation(src, references: [libRef], options: TestOptions.DebugExe); + CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext, options: TestOptions.DebugExe); + CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); + + comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); + comp.VerifyEmitDiagnostics( + // (7,13): error CS8652: The feature 'extensions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // 42.Member(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "42.Member").WithArguments("extensions").WithLocation(7, 13)); + } [Fact] public void This_01() From c53a2f388f04a8c7878bea714dfe94b31b9e9a68 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Fri, 13 Jun 2025 09:15:09 -0700 Subject: [PATCH 2/4] Address feedback --- .../Portable/Binder/Binder_Conversions.cs | 89 +++++++++++++++++-- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs index b4536e1a4916c..15fdf223d2bdc 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs @@ -1439,13 +1439,10 @@ static bool bindMethodGroupInvocation( { Debug.Assert(!resolution.OverloadResolutionResult.Succeeded); - resolution.OverloadResolutionResult.ReportDiagnostics( - binder: addMethodBinder, location: GetLocationForOverloadResolutionDiagnostic(syntax, expression), nodeOpt: syntax, diagnostics: diagnostics, name: WellKnownMemberNames.CollectionInitializerAddMethodName, - receiver: resolution.MethodGroup.Receiver, invokedExpression: expression, arguments: analyzedArguments, memberGroup: resolution.MethodGroup.Methods.ToImmutable(), - typeContainingConstructor: null, delegateTypeBeingInvoked: null, queryClause: null); - - addMethods = []; - result = false; + result = bindInvocationExpressionContinued( + addMethodBinder, syntax, expression, resolution.OverloadResolutionResult, resolution.AnalyzedArguments, + resolution.MethodGroup, diagnostics: diagnostics, out var addMethod); + addMethods = addMethod is null ? [] : [addMethod]; } } } @@ -1561,6 +1558,84 @@ static ImmutableArray filterOutBadGenericMethods( return resultBuilder.ToImmutableAndFree(); } + + // This is what BindInvocationExpressionContinued is doing in terms of reporting diagnostics and detecting a failure + static bool bindInvocationExpressionContinued( + Binder addMethodBinder, + SyntaxNode node, + SyntaxNode expression, + OverloadResolutionResult result, + AnalyzedArguments analyzedArguments, + MethodGroup methodGroup, + BindingDiagnosticBag diagnostics, + out MethodSymbol? addMethod) + { + Debug.Assert(node != null); + Debug.Assert(methodGroup != null); + Debug.Assert(methodGroup.Error == null); + Debug.Assert(methodGroup.Methods.Count > 0); + + var invokedAsExtensionMethod = methodGroup.IsExtensionMethodGroup; + + // We have already determined that we are not in a situation where we can successfully do + // a dynamic binding. We might be in one of the following situations: + // + // * There were dynamic arguments but overload resolution still found zero applicable candidates. + // * There were no dynamic arguments and overload resolution found zero applicable candidates. + // * There were no dynamic arguments and overload resolution found multiple applicable candidates + // without being able to find the best one. + // + // In those three situations we might give an additional error. + + if (!result.Succeeded) + { + // Since there were no argument errors to report, we report an error on the invocation itself. + result.ReportDiagnostics( + binder: addMethodBinder, location: GetLocationForOverloadResolutionDiagnostic(node, expression), nodeOpt: node, diagnostics: diagnostics, name: WellKnownMemberNames.CollectionInitializerAddMethodName, + receiver: methodGroup.Receiver, invokedExpression: expression, arguments: analyzedArguments, memberGroup: methodGroup.Methods.ToImmutable(), + typeContainingConstructor: null, delegateTypeBeingInvoked: null, queryClause: null); + + addMethod = null; + return false; + } + + // Otherwise, there were no dynamic arguments and overload resolution found a unique best candidate. + // We still have to determine if it passes final validation. + + var methodResult = result.ValidResult; + var method = methodResult.Member; + + // Tracked by https://github.com/dotnet/roslyn/issues/76130: It looks like we added a bunch of code in BindInvocationExpressionContinued at this position + // that specifically deals with new extension methods. It adjusts analyzedArguments, etc. + // It is very likely we need to do the same here. + + // It is possible that overload resolution succeeded, but we have chosen an + // instance method and we're in a static method. A careful reading of the + // overload resolution spec shows that the "final validation" stage allows an + // "implicit this" on any method call, not just method calls from inside + // instance methods. Therefore we must detect this scenario here, rather than in + // overload resolution. + + var receiver = methodGroup.Receiver; + + // Note: we specifically want to do final validation (7.6.5.1) without checking delegate compatibility (15.2), + // so we're calling MethodGroupFinalValidation directly, rather than via MethodGroupConversionHasErrors. + // Note: final validation wants the receiver that corresponds to the source representation + // (i.e. the first argument, if invokedAsExtensionMethod). + var gotError = addMethodBinder.MemberGroupFinalValidation(receiver, method, expression, diagnostics, invokedAsExtensionMethod); + + addMethodBinder.ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); + ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); + ReportDiagnosticsIfDisallowedExtension(diagnostics, method, node); + + // No use site errors, but there could be use site warnings. + // If there are any use site warnings, they have already been reported by overload resolution. + Debug.Assert(!method.HasUseSiteError, "Shouldn't have reached this point if there were use site errors."); + Debug.Assert(!method.IsRuntimeFinalizer()); + + addMethod = method; + return !gotError; + } } /// From 77a0cdc6deea1c7bd89c13d5ac069965de4942ac Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Mon, 16 Jun 2025 12:33:02 -0700 Subject: [PATCH 3/4] Address feedback --- src/Compilers/CSharp/Portable/Binder/Binder.cs | 2 +- src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.cs b/src/Compilers/CSharp/Portable/Binder/Binder.cs index bec8a6f2ae4ae..45b90a0e710ed 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.cs @@ -751,7 +751,7 @@ internal static void ReportDiagnosticsIfObsoleteInternal(BindingDiagnosticBag di internal static bool IsDisallowedExtensionInOlderLangVer(MethodSymbol symbol) { - return symbol.GetIsNewExtensionMember() && symbol.IsStatic; + return symbol.GetIsNewExtensionMember() && (symbol.IsStatic || symbol.MethodKind != MethodKind.Ordinary); } internal static void ReportDiagnosticsIfDisallowedExtension(BindingDiagnosticBag diagnostics, MethodSymbol method, SyntaxNode syntax) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs index 78fdf8c093bfa..1e81912b14d95 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs @@ -334,7 +334,6 @@ private BoundExpression BindCompoundAssignment(AssignmentExpressionSyntax node, ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); - Debug.Assert(!IsDisallowedExtensionInOlderLangVer(method)); inPlaceResult = new BoundCompoundAssignmentOperator( node, @@ -2648,7 +2647,6 @@ private BoundExpression BindIncrementOperator(ExpressionSyntax node, ExpressionS ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); - Debug.Assert(!IsDisallowedExtensionInOlderLangVer(method)); inPlaceResult = new BoundIncrementOperator( node, From 75011c8bf7987647ec233a68b6d226bd0bd431f9 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Mon, 16 Jun 2025 12:45:50 -0700 Subject: [PATCH 4/4] Address feedback --- .../CSharp/Test/Emit3/Semantics/ExtensionTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs index 2a804aa871457..85fa3eb67184a 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs @@ -20971,10 +20971,10 @@ class C { } CompileAndVerify(comp, expectedOutput: "(42, 43)").VerifyDiagnostics(); comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.Regular13); - comp.VerifyEmitDiagnostics(); + CompileAndVerify(comp, expectedOutput: "(42, 43)").VerifyDiagnostics(); comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); - comp.VerifyEmitDiagnostics(); + CompileAndVerify(comp, expectedOutput: "(42, 43)").VerifyDiagnostics(); } [Fact] @@ -22600,7 +22600,7 @@ public int Property Diagnostic(ErrorCode.ERR_FeatureInPreview, "Property").WithArguments("extensions").WithLocation(2, 12)); comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); - comp.VerifyEmitDiagnostics(); + CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); } [Fact] @@ -23168,7 +23168,7 @@ public static class E Diagnostic(ErrorCode.ERR_FeatureInPreview, "Property").WithArguments("extensions").WithLocation(1, 15)); comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); - comp.VerifyEmitDiagnostics(); + CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); } [Fact] @@ -23322,7 +23322,7 @@ public static class E Diagnostic(ErrorCode.ERR_FeatureInPreview, "Property").WithArguments("extensions").WithLocation(1, 20)); comp = CreateCompilation(src, references: [libRef], parseOptions: TestOptions.RegularNext); - comp.VerifyEmitDiagnostics(); + CompileAndVerify(comp, expectedOutput: "property").VerifyDiagnostics(); } [Fact]