diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 0487dac7031d6..2c278a0885127 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7592,4 +7592,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Nullability of reference types in return type doesn't match interceptable method. + + A nameof operator cannot be intercepted. + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index f1e821455f710..21ec5d24387f2 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -2288,7 +2288,7 @@ internal void AddInterception(string filePath, int line, int character, Location factoryArgument: (AttributeLocation: attributeLocation, Interceptor: interceptor)); } - internal (Location AttributeLocation, MethodSymbol Interceptor)? TryGetInterceptor(Location? callLocation, BindingDiagnosticBag diagnostics) + internal (Location AttributeLocation, MethodSymbol Interceptor)? TryGetInterceptor(Location? callLocation) { if (_interceptions is null || callLocation is null) { @@ -2306,10 +2306,9 @@ internal void AddInterception(string filePath, int line, int character, Location return oneInterception; } - // We don't normally reach this branch in batch compilation, because we would have already reported an error after the declaration phase. - // One scenario where we may reach this is when validating used assemblies, which performs lowering of method bodies even if declaration errors would be reported. - // See 'CSharpCompilation.GetCompleteSetOfUsedAssemblies'. - diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, callLocation, this.SourceModule.Name, new LocalizableResourceString(nameof(CSharpResources.ERR_DuplicateInterceptor), CodeAnalysisResources.ResourceManager, typeof(CodeAnalysisResources))); + // Duplicate interceptors is an error in the declaration phase. + // This method is only expected to be called if no such errors are present. + throw ExceptionUtilities.Unreachable(); } return null; @@ -3274,8 +3273,6 @@ internal override bool CompileMethods( bool hasDeclarationErrors = !FilterAndAppendDiagnostics(diagnostics, GetDiagnostics(CompilationStage.Declare, true, cancellationToken), excludeDiagnostics, cancellationToken); excludeDiagnostics?.Free(); - hasDeclarationErrors |= CheckDuplicateInterceptions(diagnostics); - // TODO (tomat): NoPIA: // EmbeddedSymbolManager.MarkAllDeferredSymbolsAsReferenced(this) @@ -3416,8 +3413,8 @@ private bool CheckDuplicateFilePaths(DiagnosticBag diagnostics) return visitor.CheckDuplicateFilePathsAndFree(SyntaxTrees, GlobalNamespace); } - /// if duplicate interceptors are present in the compilation. Otherwise, . - private bool CheckDuplicateInterceptions(DiagnosticBag diagnostics) + /// if duplicate interceptions are present in the compilation. Otherwise, . + internal bool CheckDuplicateInterceptions(BindingDiagnosticBag diagnostics) { if (_interceptions is null) { diff --git a/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs b/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs index 5f8259359fa82..655f8dbc89f6d 100644 --- a/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs +++ b/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs @@ -120,10 +120,7 @@ public static void CompileMethodBodies( Debug.Assert(diagnostics != null); Debug.Assert(diagnostics.DiagnosticBag != null); - // PROTOTYPE(ic): - // - Move check for duplicate interceptions in here. - // - Change lowering to throw on duplicates. - // - Test no duplicate error given when emitting a ref assembly. + hasDeclarationErrors |= compilation.CheckDuplicateInterceptions(diagnostics); if (compilation.PreviousSubmission != null) { diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index dd15a76e93da6..1837e9ca2ee51 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2216,6 +2216,7 @@ internal enum ErrorCode ERR_InterceptorLineCharacterMustBePositive = 27020, WRN_NullabilityMismatchInReturnTypeOnInterceptor = 27021, WRN_NullabilityMismatchInParameterTypeOnInterceptor = 27022, + ERR_InterceptorCannotInterceptNameof = 27023, #endregion diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index c78d60a2a6d3b..3575d881959c1 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -595,6 +595,7 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_InterceptorScopedMismatch: case ErrorCode.WRN_NullabilityMismatchInReturnTypeOnInterceptor: case ErrorCode.WRN_NullabilityMismatchInParameterTypeOnInterceptor: + case ErrorCode.ERR_InterceptorCannotInterceptNameof: // Update src\EditorFeatures\CSharp\LanguageServer\CSharpLspBuildOnlyDiagnostics.cs // whenever new values are added here. return true; diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs index 53cb6205fd676..7f22df8bfc3ba 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs @@ -222,6 +222,16 @@ private PEModuleBuilder? EmitModule private BoundExpression? VisitExpressionImpl(BoundExpression node) { + if (node is BoundNameOfOperator nameofOperator) + { + Debug.Assert(!nameofOperator.WasCompilerGenerated); + var nameofIdentiferSyntax = (IdentifierNameSyntax)((InvocationExpressionSyntax)nameofOperator.Syntax).Expression; + if (this._compilation.TryGetInterceptor(nameofIdentiferSyntax.Location) is not null) + { + this._diagnostics.Add(ErrorCode.ERR_InterceptorCannotInterceptNameof, nameofIdentiferSyntax.Location); + } + } + ConstantValue? constantValue = node.ConstantValueOpt; if (constantValue != null) { diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index aa8f81bb2bc6b..f879d57060fbf 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -145,7 +145,7 @@ private void InterceptCallAndAdjustArguments( // Add assertions for the possible shapes of calls which could come through this method. // When the BoundCall shape changes in the future, force developer to decide what to do here. - if (this._compilation.TryGetInterceptor(interceptableLocation, _diagnostics) is not var (attributeLocation, interceptor)) + if (this._compilation.TryGetInterceptor(interceptableLocation) is not var (attributeLocation, interceptor)) { // The call was not intercepted. return; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index e1fee3b2c46a9..1a2ddbb550c77 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -13,6 +13,7 @@ using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Shared.Collections; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Symbols @@ -988,9 +989,28 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments } var syntaxTrees = DeclaringCompilation.SyntaxTrees; - // PROTOTYPE(ic): consider avoiding an array allocation here, on the assumption that 1 matching tree is the success (common) case, 0 is the most common error case, and 2 or more is much more rare. - var matchingTrees = syntaxTrees.WhereAsArray(static (tree, filePath) => tree.FilePath == filePath, filePath); - if (matchingTrees is []) + SyntaxTree? matchingTree = null; + // PROTOTYPE(ic): we need to resolve the paths before comparing (i.e. respect /pathmap). + // At that time, we should look at caching the resolved paths for the trees in a set (or maybe a Map>). + // so we can reduce the cost of these checks. + foreach (var tree in syntaxTrees) + { + if (tree.FilePath == filePath) + { + if (matchingTree == null) + { + matchingTree = tree; + // need to keep searching in case we find another tree with the same path + } + else + { + diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), filePath); + return; + } + } + } + + if (matchingTree == null) { var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath.EndsWith(filePath), filePath); if (suffixMatch != null) @@ -1005,12 +1025,6 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } - if (matchingTrees is not [var matchingTree]) - { - diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), filePath); - return; - } - // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. int lineNumberZeroBased = lineNumberOneBased - 1; int characterNumberZeroBased = characterNumberOneBased - 1; diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 13fece9ffe045..de8778737ac1c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index cef6a34a19359..b06dfcfa5ea8e 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 4e45b5edf0431..cef2aff07161a 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 8cb87827290e7..d9e3c32c66cb0 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 3269ffea46e56..164c3ffff757d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index 3491846c32a27..1296bbcdc7855 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index e3698a2235561..ade22e805992c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index c916fb98f72b2..6186fa364d4ae 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 3eca53390777d..1dc215f4cdb2b 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index 7a1c548bfcedf..60719aca6ced7 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index e30126427a68d..2c1b6686bd6f6 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index df4783e6ee001..3547b0400b008 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index 48915d7bb9113..1c6197b32db64 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -857,6 +857,11 @@ Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + A nameof operator cannot be intercepted. + A nameof operator cannot be intercepted. + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 602f71d3280ad..b352f04e79e9d 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; @@ -13,20 +14,6 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; public class InterceptorsTests : CSharpTestBase { - // PROTOTYPE(ic): Ensure that all `MethodSymbol.IsInterceptable` implementations have test coverage. - - // PROTOTYPE(ic): Possible test cases: - // - // * Intercept instance method with instance method in same class, base class, derived class - // * Intercept with extern method - // * Intercept an abstract or interface method - // * Intercept a virtual or overridden method - // * Intercept a non-extension call to a static method with a static method when one or both are extension methods - // * Intercept a struct instance method with an extension method with by-value / by-ref this parameter - // * An explicit interface implementation marked as interceptable - - // PROTOTYPE(ic): test intercepting an extension method with a non-extension method. Perhaps should be an error for simplicity even if calling in non-reduced form. - private static readonly (string, string) s_attributesSource = (""" namespace System.Runtime.CompilerServices; @@ -321,6 +308,38 @@ static class D verifier.VerifyDiagnostics(); } + [Fact] + public void InterceptableExtensionMethod_InterceptorExtensionMethod_NormalForm() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 { } + + static class Program + { + [Interceptable] + public static I1 InterceptableMethod(this I1 i1, string param) { Console.Write("interceptable " + param); return i1; } + + public static void Main() + { + var c = new C(); + InterceptableMethod(c, "call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 9)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); + verifier.VerifyDiagnostics(); + } + [Fact] public void InterceptableInstanceMethod_InterceptorExtensionMethod() { @@ -595,6 +614,264 @@ public static void M1() { } Diagnostic(ErrorCode.ERR_DuplicateInterceptor, @"InterceptsLocation(""Program.cs"", 3, 3)").WithLocation(14, 6)); } + [Fact] + public void InterceptorVirtual_01() + { + // Intercept a method call with a call to a virtual method on the same type. + var source = """ + using System.Runtime.CompilerServices; + using System; + + C c = new C(); + c.M(); + + c = new D(); + c.M(); + + class C + { + [Interceptable] + public void M() => throw null!; + + [InterceptsLocation("Program.cs", 5, 3)] + [InterceptsLocation("Program.cs", 8, 3)] + public virtual void Interceptor() => Console.Write("C"); + } + + class D : C + { + public override void Interceptor() => Console.Write("D"); + } + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Method)] + public sealed class InterceptableAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) + { + } + } + } + """; + + var verifier = CompileAndVerify((source, "Program.cs"), expectedOutput: "CD"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InterceptorVirtual_02() + { + // Intercept a call with a virtual method call on the base type. + var source = """ + using System.Runtime.CompilerServices; + using System; + + D d = new D(); + d.M(); + + class C + { + [InterceptsLocation("Program.cs", 5, 3)] + public virtual void Interceptor() => throw null!; + } + + class D : C + { + [Interceptable] + public void M() => throw null!; + + public override void Interceptor() => throw null!; + } + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Method)] + public sealed class InterceptableAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) + { + } + } + } + """; + + var comp = CreateCompilation((source, "Program.cs")); + comp.VerifyEmitDiagnostics( + // Program.cs(9,6): error CS27011: Interceptor must have a 'this' parameter matching parameter 'D this' on 'D.M()'. + // [InterceptsLocation("Program.cs", 5, 3)] + Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 5, 3)").WithArguments("D this", "D.M()").WithLocation(9, 6)); + } + + [Fact] + public void InterceptorOverride_01() + { + // Intercept a call with a call to an override method on a derived type. + var source = """ + using System.Runtime.CompilerServices; + using System; + + D d = new D(); + d.M(); + + class C + { + [Interceptable] + public void M() => throw null!; + + public virtual void Interceptor() => throw null!; + } + + class D : C + { + [InterceptsLocation("Program.cs", 5, 3)] // 1 + public override void Interceptor() => throw null!; + } + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Method)] + public sealed class InterceptableAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) + { + } + } + } + """; + + var comp = CreateCompilation((source, "Program.cs")); + comp.VerifyEmitDiagnostics( + // Program.cs(17,6): error CS27011: Interceptor must have a 'this' parameter matching parameter 'C this' on 'C.M()'. + // [InterceptsLocation("Program.cs", 5, 3)] // 1 + Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 5, 3)").WithArguments("C this", "C.M()").WithLocation(17, 6)); + } + + [Fact] + public void InterceptorOverride_02() + { + // Intercept a call with an override method on the same type. + var source = """ + using System.Runtime.CompilerServices; + using System; + + D d = new D(); + d.M(); + + class C + { + public virtual void Interceptor() => throw null!; + } + + class D : C + { + [Interceptable] + public void M() => throw null!; + + [InterceptsLocation("Program.cs", 5, 3)] + public override void Interceptor() => Console.Write(1); + } + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Method)] + public sealed class InterceptableAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) + { + } + } + } + """; + + var verifier = CompileAndVerify((source, "Program.cs"), expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void EmitMetadataOnly_01() + { + // We can emit a ref assembly even though there are duplicate interceptions. + var source = """ + using System.Runtime.CompilerServices; + + class C + { + public static void Main() + { + C.M(); + } + + [Interceptable] + public static void M() { } + } + + class D + { + [InterceptsLocation("Program.cs", 7, 11)] + public static void M1() { } + + [InterceptsLocation("Program.cs", 7, 11)] + public static void M2() { } + } + """; + + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, emitOptions: EmitOptions.Default.WithEmitMetadataOnly(true)); + verifier.VerifyDiagnostics(); + + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(16,6): error CS27016: The indicated call is intercepted multiple times. + // [InterceptsLocation("Program.cs", 7, 11)] + Diagnostic(ErrorCode.ERR_DuplicateInterceptor, @"InterceptsLocation(""Program.cs"", 7, 11)").WithLocation(16, 6), + // Program.cs(19,6): error CS27016: The indicated call is intercepted multiple times. + // [InterceptsLocation("Program.cs", 7, 11)] + Diagnostic(ErrorCode.ERR_DuplicateInterceptor, @"InterceptsLocation(""Program.cs"", 7, 11)").WithLocation(19, 6)); + } + + [Fact] + public void EmitMetadataOnly_02() + { + // We can't emit a ref assembly when a problem is found with an InterceptsLocationAttribute in the declaration phase. + // Strictly, we should perhaps allow this emit anyway, but it doesn't feel urgent to do so. + var source = """ + using System.Runtime.CompilerServices; + + C.M(); + + class C + { + [Interceptable] + public static void M() { } + } + + class D + { + [InterceptsLocation("Program.cs", 3, 4)] + public static void M1() { } + + } + """; + + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics(EmitOptions.Default.WithEmitMetadataOnly(true), + // Program.cs(13,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '('. + // [InterceptsLocation("Program.cs", 3, 4)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 3, 4)").WithArguments("(").WithLocation(13, 6)); + } + [Fact] public void InterceptsLocationFromMetadata() { @@ -700,9 +977,92 @@ public static void Interceptor1(object param) { } } """; var compilation = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); - // PROTOTYPE(ic): this is syntactically an invocation but doesn't result in a BoundCall. - // we should produce an error here, probably during lowering. compilation.VerifyEmitDiagnostics( + // Program.cs(7,13): error CS27023: A nameof operator cannot be intercepted. + // _ = nameof(Main); + Diagnostic(ErrorCode.ERR_InterceptorCannotInterceptNameof, "nameof").WithLocation(7, 13) + ); + } + + [Fact] + public void InterceptableNameof_MethodCall() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + static class Program + { + public static void Main() + { + _ = nameof(F); + } + + private static object F = 1; + + [Interceptable] + public static string nameof(object param) => throw null!; + } + + static class D + { + [InterceptsLocation("Program.cs", 8, 13)] + public static string Interceptor1(object param) + { + Console.Write(1); + return param.ToString(); + } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InterceptableDoubleUnderscoreReservedIdentifiers() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + static class Program + { + public static void Main() + { + M1(__arglist(1, 2, 3)); + + int i = 0; + TypedReference tr = __makeref(i); + ref int ri = ref __refvalue(tr, int); + Type t = __reftype(tr); + } + + static void M1(__arglist) { } + } + + static class D + { + [InterceptsLocation("Program.cs", 8, 12)] // __arglist + [InterceptsLocation("Program.cs", 11, 29)] // __makeref + [InterceptsLocation("Program.cs", 12, 26)] // __refvalue + [InterceptsLocation("Program.cs", 13, 18)] // __reftype + public static void Interceptor1(int x, int y, int z) { } + } + """; + var compilation = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + compilation.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '__arglist'. + // [InterceptsLocation("Program.cs", 8, 12)] // __arglist + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 8, 12)").WithArguments("__arglist").WithLocation(21, 6), + // Program.cs(22,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '__makeref'. + // [InterceptsLocation("Program.cs", 11, 29)] // __makeref + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 11, 29)").WithArguments("__makeref").WithLocation(22, 6), + // Program.cs(23,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '__refvalue'. + // [InterceptsLocation("Program.cs", 12, 26)] // __refvalue + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 12, 26)").WithArguments("__refvalue").WithLocation(23, 6), + // Program.cs(24,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '__reftype'. + // [InterceptsLocation("Program.cs", 13, 18)] // __reftype + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 13, 18)").WithArguments("__reftype").WithLocation(24, 6) ); } @@ -713,7 +1073,7 @@ public void InterceptableDelegateInvocation() using System.Runtime.CompilerServices; using System; - C.M(() => Console.Write(0)); + C.M(() => Console.Write(1)); static class C { @@ -726,13 +1086,11 @@ public static void M(Action action) static class D { [InterceptsLocation("Program.cs", 10, 9)] - public static void Interceptor1(this Action action) { Console.Write(1); } + public static void Interceptor1(this Action action) { action(); Console.Write(2); } } """; - var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "1"); - // PROTOTYPE(ic): perhaps give a more specific error here. - // If/when we change "missing InterceptableAttribute" to an error, we might not need any specific error, because user cannot attribute the Invoke method. - // I don't think we intend for delegate Invoke to be interceptable, but it doesn't seem harmful to allow it. + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "12"); + // PROTOTYPE(ic): drop warning when InterceptableAttribute is removed from the design. verifier.VerifyDiagnostics( // Program.cs(16,6): warning CS27000: Call to 'Action.Invoke()' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. // [InterceptsLocation("Program.cs", 10, 9)] @@ -832,6 +1190,31 @@ static class D Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 5, 3)").WithArguments("C c", "D.InterceptableMethod(C)").WithLocation(14, 6)); } + [Fact] + public void InterceptableExtensionMethod_InterceptorStaticMethod_NormalForm() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + var c = new C(); + D.InterceptableMethod(c); + + class C { } + + static class D + { + [Interceptable] + public static void InterceptableMethod(this C c) => throw null!; + + [InterceptsLocation("Program.cs", 5, 3)] + public static void Interceptor1(C c) => Console.Write(1); + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + [Fact] public void InterceptableStaticMethod_InterceptorInstanceMethod() { diff --git a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs index b4388fa679963..d4ba2a87665f8 100644 --- a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs +++ b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs @@ -2951,6 +2951,7 @@ public void TestIsBuildOnlyDiagnostic() case ErrorCode.ERR_InterceptorScopedMismatch: case ErrorCode.WRN_NullabilityMismatchInReturnTypeOnInterceptor: case ErrorCode.WRN_NullabilityMismatchInParameterTypeOnInterceptor: + case ErrorCode.ERR_InterceptorCannotInterceptNameof: Assert.True(isBuildOnly, $"Check failed for ErrorCode.{errorCode}"); break; diff --git a/src/Compilers/Test/Core/Diagnostics/DiagnosticExtensions.cs b/src/Compilers/Test/Core/Diagnostics/DiagnosticExtensions.cs index 0211f83eddabc..3d15b39aae17b 100644 --- a/src/Compilers/Test/Core/Diagnostics/DiagnosticExtensions.cs +++ b/src/Compilers/Test/Core/Diagnostics/DiagnosticExtensions.cs @@ -362,7 +362,7 @@ public static ImmutableArray GetEmitDiagnostics( IEnumerable manifestResources = null) where TCompilation : Compilation { - var pdbStream = MonoHelpers.IsRunningOnMono() ? null : new MemoryStream(); + var pdbStream = MonoHelpers.IsRunningOnMono() || options?.EmitMetadataOnly == true ? null : new MemoryStream(); return c.Emit(new MemoryStream(), pdbStream: pdbStream, options: options, manifestResources: manifestResources).Diagnostics; } diff --git a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs index 48cd732ab48dc..bd2d532127a09 100644 --- a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs +++ b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs @@ -53,7 +53,8 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer "CS27018", // ErrorCode.ERR_InterceptorNotAccessible "CS27019", // ErrorCode.ERR_InterceptorScopedMismatch "CS27021", // ErrorCode.WRN_NullabilityMismatchInReturnTypeOnInterceptor - "CS27022" // ErrorCode.WRN_NullabilityMismatchInParameterTypeOnInterceptor + "CS27022", // ErrorCode.WRN_NullabilityMismatchInParameterTypeOnInterceptor + "CS27023" // ErrorCode.ERR_InterceptorCannotInterceptNameof )] internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics {