From fb564f3101c43acb391820ff7bdfe368166d4c8c Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 20 Mar 2023 14:44:09 -0700 Subject: [PATCH 01/49] Add method property IsInterceptable --- .../Portable/Symbols/ErrorMethodSymbol.cs | 2 + .../FunctionPointerMethodSymbol.cs | 1 + .../Symbols/Metadata/PE/PEMethodSymbol.cs | 26 ++ .../CSharp/Portable/Symbols/MethodSymbol.cs | 2 + .../Symbols/ReducedExtensionMethodSymbol.cs | 3 + .../Symbols/SignatureOnlyMethodSymbol.cs | 4 + .../SourceMethodSymbolWithAttributes.cs | 6 + .../SynthesizedEntryPointSymbol.cs | 2 + .../SynthesizedGlobalMethodSymbol.cs | 2 + .../SynthesizedInstanceMethodSymbol.cs | 2 + .../SynthesizedIntrinsicOperatorSymbol.cs | 7 + .../SynthesizedStaticConstructor.cs | 2 + .../Symbols/Wrapped/WrappedMethodSymbol.cs | 9 + .../Semantic/Semantics/InterceptorsTests.cs | 237 ++++++++++++++++++ .../Core/Portable/MetadataReader/PEModule.cs | 4 + .../Attributes/AttributeDescription.cs | 6 + .../CommonMethodWellKnownAttributeData.cs | 20 ++ 17 files changed, 335 insertions(+) create mode 100644 src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs diff --git a/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs index 595fbd4211769..ef7005725a1bd 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs @@ -276,6 +276,8 @@ internal override bool GenerateDebugInfo internal sealed override bool IsNullableAnalysisEnabled() => false; + internal override bool IsInterceptable => false; + protected override bool HasSetsRequiredMembersImpl { get diff --git a/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs index 71dffbb25d31a..6b455b053ec11 100644 --- a/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs @@ -831,6 +831,7 @@ public override bool IsVararg public override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None; internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) => false; + internal sealed override bool IsInterceptable => false; internal sealed override UnmanagedCallersOnlyAttributeData? GetUnmanagedCallersOnlyAttributeData(bool forceComplete) => null; internal override bool GenerateDebugInfo => throw ExceptionUtilities.Unreachable(); diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs index 6bd5fe8a05be1..f01b065b5747f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs @@ -106,6 +106,8 @@ private struct PackedFlags private const int HasSetsRequiredMembersPopulatedBit = 0x1 << 28; private const int IsUnscopedRefBit = 0x1 << 29; private const int IsUnscopedRefPopulatedBit = 0x1 << 30; + private const int IsInterceptableBit = 0x1 << 31; + private const int IsInterceptablePopulatedBit = 0x1 << 32; private int _bits; @@ -146,6 +148,8 @@ public MethodKind MethodKind public bool HasSetsRequiredMembersPopulated => (_bits & HasSetsRequiredMembersPopulatedBit) != 0; public bool IsUnscopedRef => (_bits & IsUnscopedRefBit) != 0; public bool IsUnscopedRefPopulated => (_bits & IsUnscopedRefPopulatedBit) != 0; + public bool IsInterceptable => (_bits & IsInterceptableBit) != 0; + public bool IsInterceptablePopulated => (_bits & IsInterceptablePopulatedBit) != 0; #if DEBUG static PackedFlags() @@ -268,6 +272,14 @@ public bool InitializeIsUnscopedRef(bool value) return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } + + public bool InitializeInterceptable(bool value) + { + int bitsToSet = IsInterceptablePopulatedBit; + if (value) bitsToSet |= IsInterceptableBit; + + return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); + } } /// @@ -1648,6 +1660,20 @@ internal sealed override bool HasUnscopedRefAttribute return _packedFlags.IsUnscopedRef; } } + internal sealed override bool IsInterceptable + { + get + { + if (!_packedFlags.IsInterceptablePopulated) + { + var moduleSymbol = _containingType.ContainingPEModule; + bool interceptable = moduleSymbol.Module.HasInterceptableAttribute(_handle); + _packedFlags.InitializeInterceptable(interceptable); + } + + return _packedFlags.IsInterceptable; + } + } internal sealed override bool UseUpdatedEscapeRules => ContainingModule.UseUpdatedEscapeRules; } diff --git a/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs index e359a98d68fa7..f8a9584b6cfff 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs @@ -94,6 +94,8 @@ public virtual bool IsGenericMethod /// internal virtual bool IsDirectlyExcludedFromCodeCoverage { get => false; } + internal abstract bool IsInterceptable { get; } + /// /// If a method is annotated with `[MemberNotNull(...)]` attributes, returns the list of members /// listed in those attributes. diff --git a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs index c59e90fd57611..cc038a794fa05 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs @@ -574,6 +574,9 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l internal override bool IsNullableAnalysisEnabled() => throw ExceptionUtilities.Unreachable(); + // PROTOTYPE(ic): These symbols are only used for public API and shouldn't be able to call into this + internal override bool IsInterceptable => throw ExceptionUtilities.Unreachable(); + public override bool Equals(Symbol obj, TypeCompareKind compareKind) { if ((object)this == obj) return true; diff --git a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs index 279464bc50d4f..d75ef7071bcf9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs @@ -96,6 +96,10 @@ public SignatureOnlyMethodSymbol( internal sealed override bool IsNullableAnalysisEnabled() => throw ExceptionUtilities.Unreachable(); + // PROTOTYPE(ic) + internal sealed override bool IsInterceptable => throw ExceptionUtilities.Unreachable(); + + #region Not used by MethodSignatureComparer internal override bool GenerateDebugInfo { get { throw ExceptionUtilities.Unreachable(); } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 01864120d85ea..45489d4527ac5 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -591,6 +591,10 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut diagnostics.Add(ErrorCode.ERR_UnscopedRefAttributeUnsupportedMemberTarget, arguments.AttributeSyntaxOpt.Location); } } + else if (attribute.IsTargetAttribute(this, AttributeDescription.InterceptableAttribute)) + { + arguments.GetOrCreateData().HasInterceptableAttribute = true; + } else { var compilation = this.DeclaringCompilation; @@ -1239,6 +1243,8 @@ internal override bool HasSpecialName internal sealed override bool IsDirectlyExcludedFromCodeCoverage => GetDecodedWellKnownAttributeData()?.HasExcludeFromCodeCoverageAttribute == true; + internal sealed override bool IsInterceptable => GetDecodedWellKnownAttributeData()?.HasInterceptableAttribute == true; + internal override bool RequiresSecurityObject { get diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs index e69f0c4e5eba5..d0c10c5b66771 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs @@ -198,6 +198,8 @@ public override bool IsExtensionMethod get { return false; } } + internal override bool IsInterceptable => false; + internal sealed override ObsoleteAttributeData ObsoleteAttributeData { get { return null; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs index 873180ce4361c..d528f319c2a79 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs @@ -336,6 +336,8 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l internal sealed override bool IsNullableAnalysisEnabled() => false; + internal sealed override bool IsInterceptable => false; + protected sealed override bool HasSetsRequiredMembersImpl => throw ExceptionUtilities.Unreachable(); internal sealed override bool HasUnscopedRefAttribute => false; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceMethodSymbol.cs index 28ed8edcf2b0d..0af1191744f22 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceMethodSymbol.cs @@ -42,6 +42,8 @@ public sealed override bool AreLocalsZeroed } } + internal sealed override bool IsInterceptable => false; + internal override bool TryGetThisParameter(out ParameterSymbol thisParameter) { Debug.Assert(!IsStatic); diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs index a95e0fcb29572..bf5faeca861fd 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs @@ -78,6 +78,13 @@ public override bool IsImplicitlyDeclared return true; } } + internal override sealed bool IsInterceptable + { + get + { + return false; + } + } internal override CSharpCompilation DeclaringCompilation { diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs index 4cfb133a4d306..2251657cfeffd 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs @@ -292,6 +292,8 @@ public override ImmutableArray ExplicitInterfaceImplementations internal override bool IsInitOnly => false; + internal sealed override bool IsInterceptable => false; + public sealed override bool IsImplicitlyDeclared { get diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs index 736c6fdbd9ed1..8d673f871233a 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs @@ -186,6 +186,15 @@ public override bool IsImplicitlyDeclared } } + // PROTOTYPE(ic): sealed? + internal override bool IsInterceptable + { + get + { + return UnderlyingMethod.IsInterceptable; + } + } + internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) { return UnderlyingMethod.IsMetadataVirtual(ignoreInterfaceImplementationChanges); diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs new file mode 100644 index 0000000000000..31ecc8fb5b5d7 --- /dev/null +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -0,0 +1,237 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; + +public class InterceptorsTests : CSharpTestBase +{ + private static (string, string) s_attributesSource = (""" + namespace System.Runtime.CompilerServices; + + [AttributeUsage(AttributeTargets.Method)] + public sealed class InterceptableAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int column) + { + } + } + """, "attributes.cs"); + + [Fact] + public void IsInterceptable() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod() { Console.Write("interceptable"); } + + public static void NotInterceptable() { Console.Write("not interceptable"); } + } + """; + + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, symbolValidator: verify, sourceSymbolValidator: verify); + verifier.VerifyDiagnostics(); + + void verify(ModuleSymbol module) + { + var method = module.GlobalNamespace.GetMember("C.InterceptableMethod"); + Assert.True(method.IsInterceptable); + + method = module.GlobalNamespace.GetMember("C.NotInterceptable"); + Assert.False(method.IsInterceptable); + } + } + + [Fact] + public void StaticInterceptable_StaticInterceptor_NoParameters() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod() { Console.Write("interceptable"); } + + public static void Main() + { + InterceptableMethod(); + } + } + + class D + { + [InterceptsLocation("Program.cs", 10, 8)] + public static void Interceptor1() { Console.Write("interceptor 1"); } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor 1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InterceptableExtensionMethod_InterceptorExtensionMethod() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + 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 InterceptableExtensionMethod_InterceptorExtensionMethod1() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 { } + + static class Program + { + [Interceptable] + public static I1 InterceptableMethod(this I1 i1, Delegate param) { Console.Write("interceptable " + param); return i1; } + + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + public static I1 InterceptorProgram1410(this I1 i1, Delegate param) { Console.Write("interceptor " + param); return i1; } + + + [InterceptsLocation("Program.cs", 14, 10)] // prototype only exact location + public static I1 InterceptorBind1(this I1 i1, Concrete c) + { + Log("starting thing"); + i1.InterceptableMethod(); + Log("ending thing"); + + coll.Select().Where().ToList(); // no-ops, pass things through. permit different return types? + } + } + """; + // look for Castle.DynamicProxy, AOP frameworks that work at runtime + // IInterceptor + // Configuration binding for ASP.NET + // Regex.IsMatch("abc"), multiple calls with same string + // System.Text.Json has AOP limitations. Intercept call to [de]serialize + // Dependency injection. need internals for libraries being referenced. + + // Bind interceptable + // Bind interceptor + + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InterceptableInstanceMethod_InterceptorExtensionMethod() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 + { + [Interceptable] + public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 10)] + 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 InterceptableExtensionMethod_InterceptorExtensionMethod_Sequence() + { + 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(); + c.InterceptableMethod("call site") + .InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + 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 siteinterceptable call site"); + verifier.VerifyDiagnostics(); + } +} \ No newline at end of file diff --git a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs index 478a6c3f80911..e8e4ae0a3ff3f 100644 --- a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs +++ b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs @@ -1025,6 +1025,10 @@ internal bool HasFixedBufferAttribute(EntityHandle token, out string elementType return HasStringAndIntValuedAttribute(token, AttributeDescription.FixedBufferAttribute, out elementTypeName, out bufferSize); } + internal bool HasInterceptableAttribute(EntityHandle token) + { + return FindTargetAttribute(token, AttributeDescription.InterceptableAttribute).HasValue; + } internal bool HasAccessedThroughPropertyAttribute(EntityHandle token, out string propertyName) { return HasStringValuedAttribute(token, AttributeDescription.AccessedThroughPropertyAttribute, out propertyName); diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs index 59fe257f622ad..dca8f2c12a8e9 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs @@ -179,6 +179,8 @@ static AttributeDescription() private static readonly byte[] s_signature_HasThis_Void_Type_Type_Type_Type = new byte[] { (byte)SignatureAttributes.Instance, 4, Void, TypeHandle, (byte)TypeHandleTarget.SystemType, TypeHandle, (byte)TypeHandleTarget.SystemType, TypeHandle, (byte)TypeHandleTarget.SystemType, TypeHandle, (byte)TypeHandleTarget.SystemType }; private static readonly byte[] s_signature_HasThis_Void_Type_Int32 = new byte[] { (byte)SignatureAttributes.Instance, 2, Void, TypeHandle, (byte)TypeHandleTarget.SystemType, Int32 }; + private static readonly byte[] s_signature_HasThis_Void_String_Int32_Int32 = new byte[] { (byte)SignatureAttributes.Instance, 3, Void, String, Int32, Int32 }; + private static readonly byte[] s_signature_HasThis_Void_SzArray_Boolean = new byte[] { (byte)SignatureAttributes.Instance, 1, Void, SzArray, Boolean }; private static readonly byte[] s_signature_HasThis_Void_SzArray_Byte = new byte[] { (byte)SignatureAttributes.Instance, 1, Void, SzArray, Byte }; private static readonly byte[] s_signature_HasThis_Void_SzArray_String = new byte[] { (byte)SignatureAttributes.Instance, 1, Void, SzArray, String }; @@ -222,6 +224,8 @@ static AttributeDescription() private static readonly byte[][] s_signaturesOfMemberNotNullAttribute = { s_signature_HasThis_Void_String, s_signature_HasThis_Void_SzArray_String }; private static readonly byte[][] s_signaturesOfMemberNotNullWhenAttribute = { s_signature_HasThis_Void_Boolean_String, s_signature_HasThis_Void_Boolean_SzArray_String }; private static readonly byte[][] s_signaturesOfFixedBufferAttribute = { s_signature_HasThis_Void_Type_Int32 }; + private static readonly byte[][] s_signaturesOfInterceptableAttribute = { s_signature_HasThis_Void }; + private static readonly byte[][] s_signaturesOfInterceptsLocationAttribute = { s_signature_HasThis_Void_String_Int32_Int32 }; private static readonly byte[][] s_signaturesOfPrincipalPermissionAttribute = { s_signature_HasThis_Void_SecurityAction }; private static readonly byte[][] s_signaturesOfPermissionSetAttribute = { s_signature_HasThis_Void_SecurityAction }; @@ -384,6 +388,8 @@ static AttributeDescription() internal static readonly AttributeDescription StructLayoutAttribute = new AttributeDescription("System.Runtime.InteropServices", "StructLayoutAttribute", s_signaturesOfStructLayoutAttribute); internal static readonly AttributeDescription FieldOffsetAttribute = new AttributeDescription("System.Runtime.InteropServices", "FieldOffsetAttribute", s_signaturesOfFieldOffsetAttribute); internal static readonly AttributeDescription FixedBufferAttribute = new AttributeDescription("System.Runtime.CompilerServices", "FixedBufferAttribute", s_signaturesOfFixedBufferAttribute); + internal static readonly AttributeDescription InterceptableAttribute = new AttributeDescription("System.Runtime.CompilerServices", "InterceptableAttribute", s_signaturesOfInterceptableAttribute); + internal static readonly AttributeDescription InterceptsLocationAttribute = new AttributeDescription("System.Runtime.CompilerServices", "InterceptsLocationAttribute", s_signaturesOfInterceptsLocationAttribute); internal static readonly AttributeDescription AllowNullAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "AllowNullAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription DisallowNullAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "DisallowNullAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription MaybeNullAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "MaybeNullAttribute", s_signatures_HasThis_Void_Only); diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs b/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs index a4c0da5526f9c..012a2acea407d 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs @@ -232,5 +232,25 @@ public bool HasExcludeFromCodeCoverageAttribute } #endregion + + #region InterceptableAttribute + + private bool _hasInterceptableAttribute; + public bool HasInterceptableAttribute + { + get + { + VerifySealed(expected: true); + return _hasInterceptableAttribute; + } + set + { + VerifySealed(expected: false); + _hasInterceptableAttribute = value; + SetDataStored(); + } + } + + #endregion } } From 416b438b055e135c69d0d960d6838dacc7f7cebf Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 20 Mar 2023 16:59:57 -0700 Subject: [PATCH 02/49] intercept --- .../Portable/BoundTree/BoundExpression.cs | 21 ++++++++++ .../Portable/Compilation/CSharpCompilation.cs | 39 +++++++++++++++++++ .../LocalRewriter/LocalRewriter_Call.cs | 24 +++++++++++- .../InterceptsLocationAttributeData.cs | 12 ++++++ .../MethodWellKnownAttributeData.cs | 33 ++++++++++++++++ .../Symbols/Metadata/PE/PEMethodSymbol.cs | 1 + .../SourceMethodSymbolWithAttributes.cs | 32 +++++++++++++++ .../Semantic/Semantics/InterceptorsTests.cs | 21 +++++----- .../Core/Portable/MetadataReader/PEModule.cs | 1 + .../CommonMethodWellKnownAttributeData.cs | 20 ---------- 10 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index 320953b548abe..f1e27b4b92b85 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp.Symbols; using Roslyn.Utilities; using System; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.CodeAnalysis.CSharp { @@ -238,6 +239,26 @@ public override Symbol ExpressionSymbol return this.Method; } } + + public Location? InterceptableLocation + { + get + { + if (this.Syntax is not InvocationExpressionSyntax syntax) + { + return null; + } + + if (syntax.Expression is MemberAccessExpressionSyntax memberAccess) + { + return memberAccess.Name.Location; + } + else + { + return syntax.Expression.Location; + } + } + } } internal partial class BoundTypeExpression diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index 739cf2884f80d..f3e1da3c7f485 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -2247,6 +2247,43 @@ internal void AddModuleInitializerMethod(MethodSymbol method) LazyInitializer.EnsureInitialized(ref _moduleInitializerMethods).Add(method); } + private ConcurrentSet<(InterceptsLocationAttributeData, MethodSymbol)>? _interceptions; + + internal void AddInterception(InterceptsLocationAttributeData location, MethodSymbol interceptor) + { + Debug.Assert(!_declarationDiagnosticsFrozen); + LazyInitializer.EnsureInitialized(ref _interceptions).Add((location, interceptor)); + // PROTOTYPE(ic): when all this is done, build a map and give duplicate/misplaced diagnostics which we can use in lowering. + } + + internal MethodSymbol? GetInterceptor(Location? callLocation) + { + if (_interceptions is null || callLocation is null) + { + return null; + } + + foreach (var (interceptsLocation, interceptor) in _interceptions) + { + var callLineColumn = callLocation.GetLineSpan().Span.Start; + if (interceptsLocation.FilePath == callLocation.SourceTree!.FilePath + && interceptsLocation.Line == callLineColumn.Line + && interceptsLocation.Character == callLineColumn.Character) + { + return interceptor; + } + } + + return null; + } + + internal void BuildInterceptorsMap() + { + // PROTOTYPE(ic): build a map where we can quickly lookup with a location and get a symbol. + // At this time, should report any duplicate interceptor diagnostics. + // NB: the attribute which appears lexically first wins a tie. Subsequent attributes referring to same location result in errors. + } + #endregion #region Binding @@ -3237,6 +3274,8 @@ internal override bool CompileMethods( return false; } + BuildInterceptorsMap(); + // Perform initial bind of method bodies in spite of earlier errors. This is the same // behavior as when calling GetDiagnostics() diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 0e683f5ac78a4..f85317750e05f 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -137,6 +137,26 @@ public override BoundNode VisitCall(BoundCall node) { Debug.Assert(node != null); + var method = node.Method; + + var interceptableLocation = node.InterceptableLocation; + var interceptor = this._compilation.GetInterceptor(interceptableLocation); + if (interceptor != null) + { + Debug.Assert(interceptableLocation != null); + if (!method.IsInterceptable) + { + this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + } + + // if (!MemberSignatureComparer.PartialMethodsComparer.Equals(method, interceptor)) + // { + // this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + // // PROTOTYPE(ic): permit intercepting an instance method with an extension method, etc. + // } + method = interceptor; + } + // Rewrite the receiver BoundExpression? rewrittenReceiver = VisitExpression(node.ReceiverOpt); var argRefKindsOpt = node.ArgumentRefKindsOpt; @@ -146,7 +166,7 @@ public override BoundNode VisitCall(BoundCall node) ref rewrittenReceiver, captureReceiverMode: ReceiverCaptureMode.Default, node.Arguments, - node.Method, + method, node.ArgsToParamsOpt, argRefKindsOpt, storesOpt: null, @@ -155,7 +175,7 @@ public override BoundNode VisitCall(BoundCall node) var rewrittenCall = MakeArgumentsAndCall( syntax: node.Syntax, rewrittenReceiver: rewrittenReceiver, - method: node.Method, + method: method, arguments: rewrittenArguments, argumentRefKindsOpt: argRefKindsOpt, expanded: node.Expanded, diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs new file mode 100644 index 0000000000000..bba477684b0ff --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.CSharp +{ + /// + /// Information decoded from InterceptsLocationAttribute. + /// + // PROTOTYPE(ic): record ok? + internal sealed record InterceptsLocationAttributeData(string FilePath, int Line, int Character, Location attributeLocation); +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs index e95f295b21e87..6d7d2b18e3929 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs @@ -149,5 +149,38 @@ public UnmanagedCallersOnlyAttributeData? UnmanagedCallersOnlyAttributeData SetDataStored(); } } + + private bool _hasInterceptableAttribute; + public bool HasInterceptableAttribute + { + get + { + VerifySealed(expected: true); + return _hasInterceptableAttribute; + } + set + { + VerifySealed(expected: false); + _hasInterceptableAttribute = value; + SetDataStored(); + } + } + + // PROTOTYPE(ic): do we want to expose InterceptsLocation data on the symbol itself? + // private ImmutableArray _interceptsLocations = ImmutableArray.Empty; + + // internal void AddInterceptsLocation(InterceptsLocationAttributeData interceptsLocation) + // { + // _interceptsLocations = _interceptsLocations.Add(interceptsLocation); + // } + + // public ImmutableArray InterceptsLocationsAttributeData + // { + // get + // { + // VerifySealed(expected: true); + // return _interceptsLocations; + // } + // } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs index f01b065b5747f..8c495af1a51d2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs @@ -1660,6 +1660,7 @@ internal sealed override bool HasUnscopedRefAttribute return _packedFlags.IsUnscopedRef; } } + internal sealed override bool IsInterceptable { get diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 45489d4527ac5..8e6b29bc5986d 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -595,6 +595,10 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut { arguments.GetOrCreateData().HasInterceptableAttribute = true; } + else if (attribute.IsTargetAttribute(this, AttributeDescription.InterceptsLocationAttribute)) + { + DecodeInterceptsLocationAttribute(arguments); + } else { var compilation = this.DeclaringCompilation; @@ -933,6 +937,34 @@ private void DecodeModuleInitializerAttribute(DecodeWellKnownAttributeArguments< } } + private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments arguments) + { + // PROTOTYPE(ic): more diagnostics + Debug.Assert(arguments.AttributeSyntaxOpt is object); + var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; + var attributeLocation = arguments.AttributeSyntaxOpt.Location; + + var attributeArguments = arguments.Attribute.CommonConstructorArguments; + if (attributeArguments is not [ + { Kind: not TypedConstantKind.Array, Value: string filePath }, + { Kind: not TypedConstantKind.Array, Value: int lineNumber }, + { Kind: not TypedConstantKind.Array, Value: int characterNumber }]) + { + // PROTOTYPE(ic): diagnostic + diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, attributeLocation); + return; + } + + if (!DeclaringCompilation.SyntaxTrees.Any(static (tree, filePath) => tree.FilePath == filePath, filePath)) + { + // PROTOTYPE(ic): diagnostic: no matching file + // TODO: if we normalize paths, it should probably be here. + diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, attributeLocation); + } + + DeclaringCompilation.AddInterception(new InterceptsLocationAttributeData(filePath, lineNumber, characterNumber, attributeLocation), this); + } + private void DecodeUnmanagedCallersOnlyAttribute(ref DecodeWellKnownAttributeArguments arguments) { Debug.Assert(arguments.AttributeSyntaxOpt != null); diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 31ecc8fb5b5d7..7e6904dacecf2 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -119,6 +119,7 @@ static class D [Fact] public void InterceptableExtensionMethod_InterceptorExtensionMethod1() { + // PROTOTYPE(ic): this test was more of a scratchpad, should probably delete var source = """ using System.Runtime.CompilerServices; using System; @@ -134,7 +135,7 @@ static class Program public static void Main() { var c = new C(); - c.InterceptableMethod("call site"); + c.InterceptableMethod(() => { }); } } @@ -144,15 +145,15 @@ static class D public static I1 InterceptorProgram1410(this I1 i1, Delegate param) { Console.Write("interceptor " + param); return i1; } - [InterceptsLocation("Program.cs", 14, 10)] // prototype only exact location - public static I1 InterceptorBind1(this I1 i1, Concrete c) - { - Log("starting thing"); - i1.InterceptableMethod(); - Log("ending thing"); - - coll.Select().Where().ToList(); // no-ops, pass things through. permit different return types? - } + // [InterceptsLocation("Program.cs", 14, 10)] // prototype only exact location + // public static I1 InterceptorBind1(this I1 i1, Concrete c) + // { + // Log("starting thing"); + // i1.InterceptableMethod(); + // Log("ending thing"); + + // coll.Select().Where().ToList(); // no-ops, pass things through. permit different return types? + // } } """; // look for Castle.DynamicProxy, AOP frameworks that work at runtime diff --git a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs index e8e4ae0a3ff3f..0e8c83d76fd2d 100644 --- a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs +++ b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs @@ -1029,6 +1029,7 @@ internal bool HasInterceptableAttribute(EntityHandle token) { return FindTargetAttribute(token, AttributeDescription.InterceptableAttribute).HasValue; } + internal bool HasAccessedThroughPropertyAttribute(EntityHandle token, out string propertyName) { return HasStringValuedAttribute(token, AttributeDescription.AccessedThroughPropertyAttribute, out propertyName); diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs b/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs index 012a2acea407d..a4c0da5526f9c 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/CommonMethodWellKnownAttributeData.cs @@ -232,25 +232,5 @@ public bool HasExcludeFromCodeCoverageAttribute } #endregion - - #region InterceptableAttribute - - private bool _hasInterceptableAttribute; - public bool HasInterceptableAttribute - { - get - { - VerifySealed(expected: true); - return _hasInterceptableAttribute; - } - set - { - VerifySealed(expected: false); - _hasInterceptableAttribute = value; - SetDataStored(); - } - } - - #endregion } } From d204f305b474d320cd2a03469a9931d12b7abf8e Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 21 Mar 2023 11:57:01 -0700 Subject: [PATCH 03/49] intercept an instance call with an extension method --- .../LocalRewriter/LocalRewriter_Call.cs | 106 ++++++++++++++---- .../Symbols/MemberSignatureComparer.cs | 13 +++ .../Semantic/Semantics/InterceptorsTests.cs | 55 +-------- 3 files changed, 101 insertions(+), 73 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index f85317750e05f..1b3fd102009fe 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -133,41 +133,107 @@ private void EmbedIfNeedTo(BoundExpression receiver, ImmutableArray arguments, + ImmutableArray argsToParamsOpt, + ImmutableArray argumentRefKindsOpt, + bool invokedAsExtensionMethod + ) InterceptCallAndAdjustArguments(BoundCall node) { - Debug.Assert(node != null); - var method = node.Method; - + var receiverOpt = node.ReceiverOpt; + var arguments = node.Arguments; + var argsToParamsOpt = node.ArgsToParamsOpt; + var argumentRefKindsOpt = node.ArgumentRefKindsOpt; + var invokedAsExtensionMethod = node.InvokedAsExtensionMethod; + var interceptableLocation = node.InterceptableLocation; var interceptor = this._compilation.GetInterceptor(interceptableLocation); - if (interceptor != null) + if (interceptor is null) + { + // PROTOTYPE(ic): if an interceptor doesn't refer to anything, we want to report it. + // that means we need some way of flagging when an interceptor is used. + return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + } + + Debug.Assert(interceptableLocation != null); + if (!method.IsInterceptable) + { + this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + } + + if (interceptor.Arity != 0) // PROTOTYPE(ic): could check these constant rules at the original attribute usage site. + { + // PROTOTYPE(ic): for now, let's disallow type arguments on the method or containing types. + // eventually, we could consider doing a type argument inference. + this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + } + + var needToReduce = receiverOpt != null && interceptor.IsExtensionMethod; + var symbolForCompare = needToReduce ? ReducedExtensionMethodSymbol.Create(interceptor, receiverOpt!.Type, _compilation) : interceptor; + if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare)) + { + this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + } + + if (needToReduce) { - Debug.Assert(interceptableLocation != null); - if (!method.IsInterceptable) + Debug.Assert(!method.IsStatic); + arguments = arguments.Insert(0, receiverOpt!); + receiverOpt = null; + + var thisParameter = method.ThisParameter; + + if (!argsToParamsOpt.IsDefault) + { + var argsToParamsBuilder = ImmutableArray.CreateBuilder(argsToParamsOpt.Length + 1); + argsToParamsBuilder.Add(0); // argument 0 always corresponds to parameter 0 (extension this) + foreach (var paramOrdinal in argsToParamsOpt) + { + // the param that each argument *really* refers to is pushed forward by 1. + // e.g. we're adapting: + // void Type.Method(int param) { } + // void Ext.Method(this Type type, int param) { } + argsToParamsBuilder.Add(paramOrdinal + 1); + } + + argsToParamsOpt = argsToParamsBuilder.MoveToImmutable(); + } + + if (!argumentRefKindsOpt.IsDefault) { - this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + argumentRefKindsOpt = argumentRefKindsOpt.Insert(0, thisParameter.RefKind); } - // if (!MemberSignatureComparer.PartialMethodsComparer.Equals(method, interceptor)) - // { - // this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); - // // PROTOTYPE(ic): permit intercepting an instance method with an extension method, etc. - // } - method = interceptor; + invokedAsExtensionMethod = true; } + method = interceptor; + + return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + } + + public override BoundNode VisitCall(BoundCall node) + { + Debug.Assert(node != null); + + var (method, receiverOpt, arguments, argsToParamsOpt, argRefKindsOpt, invokedAsExtensionMethod) = InterceptCallAndAdjustArguments(node); + // Rewrite the receiver BoundExpression? rewrittenReceiver = VisitExpression(node.ReceiverOpt); - var argRefKindsOpt = node.ArgumentRefKindsOpt; ArrayBuilder? temps = null; var rewrittenArguments = VisitArgumentsAndCaptureReceiverIfNeeded( ref rewrittenReceiver, captureReceiverMode: ReceiverCaptureMode.Default, - node.Arguments, + arguments, method, - node.ArgsToParamsOpt, + argsToParamsOpt, argRefKindsOpt, storesOpt: null, ref temps); @@ -178,9 +244,9 @@ public override BoundNode VisitCall(BoundCall node) method: method, arguments: rewrittenArguments, argumentRefKindsOpt: argRefKindsOpt, - expanded: node.Expanded, - invokedAsExtensionMethod: node.InvokedAsExtensionMethod, - argsToParamsOpt: node.ArgsToParamsOpt, + expanded: node.Expanded, // PROTOTYPE(ic): params differences shouldn't matter--maybe even make it an error--but we need to test + invokedAsExtensionMethod: invokedAsExtensionMethod, + argsToParamsOpt: argsToParamsOpt, resultKind: node.ResultKind, type: node.Type, temps, diff --git a/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs b/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs index 7868ec311522c..8eec24bf2da16 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs @@ -128,6 +128,19 @@ internal sealed class MemberSignatureComparer : IEqualityComparer considerRefKindDifferences: true, typeComparison: TypeCompareKind.AllIgnoreOptions); + /// + /// This instance is used to determine if an interceptor can be applied to an interceptable method. + /// NB: when a classic extension method is intercepting an instance method call, a normalization to 'ReducedExtensionMethodSymbol' must be performed first. + /// + public static readonly MemberSignatureComparer InterceptorsComparer = new MemberSignatureComparer( + considerName: false, + considerExplicitlyImplementedInterfaces: false, + considerReturnType: true, + considerTypeConstraints: true, + considerCallingConvention: false, + considerRefKindDifferences: true, + typeComparison: TypeCompareKind.ObliviousNullableModifierMatchesAny); + /// /// This instance is used to determine if a partial method implementation matches the definition, /// including differences ignored by the runtime. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 7e6904dacecf2..46c92f48719f7 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -116,59 +116,8 @@ static class D verifier.VerifyDiagnostics(); } - [Fact] - public void InterceptableExtensionMethod_InterceptorExtensionMethod1() - { - // PROTOTYPE(ic): this test was more of a scratchpad, should probably delete - var source = """ - using System.Runtime.CompilerServices; - using System; - - interface I1 { } - class C : I1 { } - - static class Program - { - [Interceptable] - public static I1 InterceptableMethod(this I1 i1, Delegate param) { Console.Write("interceptable " + param); return i1; } - - public static void Main() - { - var c = new C(); - c.InterceptableMethod(() => { }); - } - } - - static class D - { - [InterceptsLocation("Program.cs", 14, 10)] - public static I1 InterceptorProgram1410(this I1 i1, Delegate param) { Console.Write("interceptor " + param); return i1; } - - - // [InterceptsLocation("Program.cs", 14, 10)] // prototype only exact location - // public static I1 InterceptorBind1(this I1 i1, Concrete c) - // { - // Log("starting thing"); - // i1.InterceptableMethod(); - // Log("ending thing"); - - // coll.Select().Where().ToList(); // no-ops, pass things through. permit different return types? - // } - } - """; - // look for Castle.DynamicProxy, AOP frameworks that work at runtime - // IInterceptor - // Configuration binding for ASP.NET - // Regex.IsMatch("abc"), multiple calls with same string - // System.Text.Json has AOP limitations. Intercept call to [de]serialize - // Dependency injection. need internals for libraries being referenced. - - // Bind interceptable - // Bind interceptor - - var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); - verifier.VerifyDiagnostics(); - } + // PROTOTYPE(ic): test a case where the original method has type parameter constraints. + // PROTOTYPE(ic): for now we will just completely disallow type parameters in the interceptor. [Fact] public void InterceptableInstanceMethod_InterceptorExtensionMethod() From 9702a2a8c03ab3150d5ec97e46afb58a4f3244b0 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 21 Mar 2023 15:03:05 -0700 Subject: [PATCH 04/49] more --- .../CSharp/Portable/CSharpResources.resx | 36 +- .../Portable/Compilation/CSharpCompilation.cs | 4 +- .../CSharp/Portable/Errors/ErrorCode.cs | 10 + .../CSharp/Portable/Errors/ErrorFacts.cs | 6 + .../LocalRewriter/LocalRewriter_Call.cs | 9 +- .../InterceptsLocationAttributeData.cs | 2 +- .../Symbols/MemberSignatureComparer.cs | 18 +- .../SourceMethodSymbolWithAttributes.cs | 69 ++- .../Portable/xlf/CSharpResources.cs.xlf | 45 ++ .../Portable/xlf/CSharpResources.de.xlf | 45 ++ .../Portable/xlf/CSharpResources.es.xlf | 45 ++ .../Portable/xlf/CSharpResources.fr.xlf | 45 ++ .../Portable/xlf/CSharpResources.it.xlf | 45 ++ .../Portable/xlf/CSharpResources.ja.xlf | 45 ++ .../Portable/xlf/CSharpResources.ko.xlf | 45 ++ .../Portable/xlf/CSharpResources.pl.xlf | 45 ++ .../Portable/xlf/CSharpResources.pt-BR.xlf | 45 ++ .../Portable/xlf/CSharpResources.ru.xlf | 45 ++ .../Portable/xlf/CSharpResources.tr.xlf | 45 ++ .../Portable/xlf/CSharpResources.zh-Hans.xlf | 45 ++ .../Portable/xlf/CSharpResources.zh-Hant.xlf | 45 ++ .../Semantic/Semantics/InterceptorsTests.cs | 394 ++++++++++++++++++ .../CSharpLspBuildOnlyDiagnostics.cs | 3 +- 23 files changed, 1110 insertions(+), 26 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index a558255ed319d..16e9ec80dc8e4 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -106,16 +106,19 @@ - text/microsoft-resx + + text/microsoft-resx 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, + System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, + System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 <null> @@ -7502,4 +7505,31 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ A switch expression arm does not begin with a 'case' keyword. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + Cannot intercept: compilation does not contain a file with path '{0}'. + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + \ 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 f3e1da3c7f485..5d7b8025a8689 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -2256,7 +2256,7 @@ internal void AddInterception(InterceptsLocationAttributeData location, MethodSy // PROTOTYPE(ic): when all this is done, build a map and give duplicate/misplaced diagnostics which we can use in lowering. } - internal MethodSymbol? GetInterceptor(Location? callLocation) + internal (InterceptsLocationAttributeData data, MethodSymbol interceptor)? GetInterceptor(Location? callLocation) { if (_interceptions is null || callLocation is null) { @@ -2270,7 +2270,7 @@ internal void AddInterception(InterceptsLocationAttributeData location, MethodSy && interceptsLocation.Line == callLineColumn.Line && interceptsLocation.Character == callLineColumn.Character) { - return interceptor; + return (interceptsLocation, interceptor); } } diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 3920aecf88c59..a6223459bca0e 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2188,6 +2188,16 @@ internal enum ErrorCode ERR_BadNullableReferenceTypeInUsingAlias = 9132, ERR_BadCaseInSwitchArm = 9134, + + // PROTOTYPE(ic): pack errors + ERR_CallNotInterceptable = 27000, + ERR_InterceptorCannotBeGeneric = 27001, + ERR_InterceptorPathNotInCompilation = 27002, + ERR_InterceptorPathNotInCompilationWithCandidate = 27003, + ERR_InterceptorPositionBadToken = 27004, + ERR_InterceptorLineOutOfRange = 27005, + ERR_InterceptorCharacterOutOfRange = 27006, + ERR_InterceptorSignatureMismatch = 27007, #endregion // Note: you will need to do the following after adding warnings: diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index feba814f28cf2..8720d9e4ccba6 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -579,6 +579,7 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_EncUpdateFailedDelegateTypeChanged: case ErrorCode.ERR_CannotBeConvertedToUtf8: case ErrorCode.ERR_FileTypeNonUniquePath: + case ErrorCode.ERR_CallNotInterceptable: // Update src\EditorFeatures\CSharp\LanguageServer\CSharpLspBuildOnlyDiagnostics.cs // whenever new values are added here. return true; @@ -2308,6 +2309,11 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_BadUnsafeInUsingDirective: case ErrorCode.ERR_BadNullableReferenceTypeInUsingAlias: case ErrorCode.ERR_BadCaseInSwitchArm: + case ErrorCode.ERR_InterceptorCannotBeGeneric: + case ErrorCode.ERR_InterceptorPathNotInCompilation: + case ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate: + case ErrorCode.ERR_InterceptorLineOutOfRange: + case ErrorCode.ERR_InterceptorCharacterOutOfRange: return false; default: // NOTE: All error codes must be explicitly handled in this switch statement diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 1b3fd102009fe..ad9f01e678c4a 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -150,8 +150,7 @@ bool invokedAsExtensionMethod var invokedAsExtensionMethod = node.InvokedAsExtensionMethod; var interceptableLocation = node.InterceptableLocation; - var interceptor = this._compilation.GetInterceptor(interceptableLocation); - if (interceptor is null) + if (this._compilation.GetInterceptor(interceptableLocation) is not var (interceptsLocationAttributeData, interceptor)) { // PROTOTYPE(ic): if an interceptor doesn't refer to anything, we want to report it. // that means we need some way of flagging when an interceptor is used. @@ -161,11 +160,11 @@ bool invokedAsExtensionMethod Debug.Assert(interceptableLocation != null); if (!method.IsInterceptable) { - this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + this._diagnostics.Add(ErrorCode.ERR_CallNotInterceptable, interceptsLocationAttributeData.AttributeLocation, method); return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); } - if (interceptor.Arity != 0) // PROTOTYPE(ic): could check these constant rules at the original attribute usage site. + if (interceptor.Arity != 0 || interceptor.ContainingType.VisitType(static (type, _, _) => type.GetArity() != 0, arg: (object?)null) is not null) { // PROTOTYPE(ic): for now, let's disallow type arguments on the method or containing types. // eventually, we could consider doing a type argument inference. @@ -177,7 +176,7 @@ bool invokedAsExtensionMethod var symbolForCompare = needToReduce ? ReducedExtensionMethodSymbol.Create(interceptor, receiverOpt!.Type, _compilation) : interceptor; if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare)) { - this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); + this._diagnostics.Add(ErrorCode.ERR_InterceptorSignatureMismatch, interceptableLocation, method, interceptor); return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); } diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs index bba477684b0ff..0404a6b1f4064 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs @@ -8,5 +8,5 @@ namespace Microsoft.CodeAnalysis.CSharp /// Information decoded from InterceptsLocationAttribute. /// // PROTOTYPE(ic): record ok? - internal sealed record InterceptsLocationAttributeData(string FilePath, int Line, int Character, Location attributeLocation); + internal sealed record InterceptsLocationAttributeData(string FilePath, int Line, int Character, Location AttributeLocation); } diff --git a/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs b/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs index 8eec24bf2da16..edf4daf768a64 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs @@ -136,9 +136,10 @@ internal sealed class MemberSignatureComparer : IEqualityComparer considerName: false, considerExplicitlyImplementedInterfaces: false, considerReturnType: true, - considerTypeConstraints: true, + considerTypeConstraints: false, considerCallingConvention: false, considerRefKindDifferences: true, + considerArity: false, typeComparison: TypeCompareKind.ObliviousNullableModifierMatchesAny); /// @@ -349,6 +350,9 @@ internal sealed class MemberSignatureComparer : IEqualityComparer // Compare the type constraints private readonly bool _considerTypeConstraints; + // Compare the arity (type parameter count) + private readonly bool _considerArity; + // Compare the full calling conventions. Still compares varargs if false. private readonly bool _considerCallingConvention; @@ -365,9 +369,11 @@ private MemberSignatureComparer( bool considerTypeConstraints, bool considerCallingConvention, bool considerRefKindDifferences, + bool considerArity = true, // PROTOTYPE(ic): make parameter required? TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers) { Debug.Assert(!considerExplicitlyImplementedInterfaces || considerName, "Doesn't make sense to consider interfaces separately from name."); + Debug.Assert(!considerTypeConstraints || considerArity, "If you consider type constraints, you must also consider arity"); _considerName = considerName; _considerExplicitlyImplementedInterfaces = considerExplicitlyImplementedInterfaces; @@ -375,6 +381,7 @@ private MemberSignatureComparer( _considerTypeConstraints = considerTypeConstraints; _considerCallingConvention = considerCallingConvention; _considerRefKindDifferences = considerRefKindDifferences; + _considerArity = considerArity; _typeComparison = typeComparison; Debug.Assert((_typeComparison & TypeCompareKind.FunctionPointerRefMatchesOutInRefReadonly) == 0, $"Rely on the {nameof(considerRefKindDifferences)} flag to set this to ensure all cases are handled."); @@ -417,9 +424,12 @@ public bool Equals(Symbol member1, Symbol member2) // NB: up to, and including, this check, we have not actually forced the (type) parameters // to be expanded - we're only using the counts. - int arity = member1.GetMemberArity(); - if ((arity != member2.GetMemberArity()) || - (member1.GetParameterCount() != member2.GetParameterCount())) + if (_considerArity && (member1.GetMemberArity() != member2.GetMemberArity())) + { + return false; + } + + if (member1.GetParameterCount() != member2.GetParameterCount()) { return false; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 8e6b29bc5986d..fd009452110f2 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.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Symbols @@ -939,27 +940,75 @@ private void DecodeModuleInitializerAttribute(DecodeWellKnownAttributeArguments< private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments arguments) { - // PROTOTYPE(ic): more diagnostics Debug.Assert(arguments.AttributeSyntaxOpt is object); - var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; - var attributeLocation = arguments.AttributeSyntaxOpt.Location; - + Debug.Assert(!arguments.Attribute.HasErrors); var attributeArguments = arguments.Attribute.CommonConstructorArguments; if (attributeArguments is not [ { Kind: not TypedConstantKind.Array, Value: string filePath }, { Kind: not TypedConstantKind.Array, Value: int lineNumber }, { Kind: not TypedConstantKind.Array, Value: int characterNumber }]) { - // PROTOTYPE(ic): diagnostic - diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, attributeLocation); + throw ExceptionUtilities.Unreachable(); + } + + var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; + var attributeLocation = arguments.AttributeSyntaxOpt.Location; + if (Arity != 0 || ContainingType.VisitType(static (type, _, _) => type.GetArity() != 0, arg: (object?)null) is not null) + { + // PROTOTYPE(ic): for now, let's disallow type arguments on the method or containing types. + // eventually, we could consider doing a type argument inference, seeing if the interceptor's type constraints are met, etc... + // but let's not bother unless somebody actually needs it. + diagnostics.Add(ErrorCode.ERR_InterceptorCannotBeGeneric, attributeLocation, this); return; } - if (!DeclaringCompilation.SyntaxTrees.Any(static (tree, filePath) => tree.FilePath == filePath, filePath)) + var syntaxTrees = DeclaringCompilation.SyntaxTrees; + var matchingTree = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath == filePath, filePath); + if (matchingTree == null) { - // PROTOTYPE(ic): diagnostic: no matching file - // TODO: if we normalize paths, it should probably be here. - diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, attributeLocation); + var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath.EndsWith(filePath), filePath); + if (suffixMatch != null) + { + diagnostics.Add(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, attributeLocation, filePath, suffixMatch.FilePath); + } + else + { + diagnostics.Add(ErrorCode.ERR_InterceptorPathNotInCompilation, attributeLocation, filePath); + } + + return; + } + + var referencedLines = matchingTree.GetText().Lines; + var referencedLineCount = referencedLines.Count; + if (lineNumber >= referencedLineCount) + { + diagnostics.Add(ErrorCode.ERR_InterceptorLineOutOfRange, attributeLocation, referencedLines.Count, lineNumber); + return; + } + + var line = referencedLines[lineNumber]; + var lineLength = line.End - line.Start; + if (characterNumber >= lineLength) + { + diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeLocation, referencedLines.Count, characterNumber); + return; + } + + // PROTOTYPE(ic): the call-site binding is going to need to look some of this stuff up again. + // are we sure this is the best place to do these kinds of checks? + var referencedPosition = line.Start + characterNumber; + var root = matchingTree.GetRoot(); + var referencedToken = root.FindToken(referencedPosition); + switch (referencedToken) + { + case { Parent: SimpleNameSyntax { Parent: MemberAccessExpressionSyntax { Parent: InvocationExpressionSyntax } memberAccess } rhs } when memberAccess.Name == rhs: + case { Parent: SimpleNameSyntax { Parent: InvocationExpressionSyntax invocation } simpleName } when invocation.Expression == simpleName: + // happy case + break; + default: + diagnostics.Add(ErrorCode.ERR_InterceptorPositionBadToken, attributeLocation, referencedToken.Text); + return; } DeclaringCompilation.AddInterception(new InterceptsLocationAttributeData(filePath, lineNumber, characterNumber, attributeLocation), this); diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index b3008e61510b8..48f466975f1a8 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -287,6 +287,11 @@ Atribut AsyncMethodBuilder je u anonymních metod bez explicitního návratového typu zakázaný. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Vstupní řetězec nelze převést na ekvivalentní reprezentaci bajtů UTF-8. {0} @@ -832,6 +837,46 @@ Vlastnosti instance v rozhraních nemůžou mít inicializátory. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' Metoda UnmanagedCallersOnly {0} nemůže implementovat člena rozhraní {1} v typu {2}. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 308fd8f619d1d..af25f4ccf4e58 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -287,6 +287,11 @@ Das AsyncMethodBuilder-Attribut ist für anonyme Methoden ohne expliziten Rückgabetyp unzulässig. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Die Eingabezeichenfolge kann nicht in die entsprechende UTF-8-Byte-Darstellung konvertiert werden. {0} @@ -832,6 +837,46 @@ Instanzeigenschaften in Schnittstellen können keine Initialisierer aufweisen. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' Die Methode „UnmanagedCallersOnly“ „{0}“ kann das Schnittstellenelement „{1}“ im Typ „{2}“ nicht implementieren. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 3f168c1c317ac..8dd7d4cd7510d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -287,6 +287,11 @@ El atributo AsyncMethodBuilder no se permite en métodos anónimos sin un tipo de valor devuelto explícito. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} La cadena de entrada no se puede convertir en la representación de bytes UTF-8 equivalente. {0} @@ -832,6 +837,46 @@ Las propiedades de la instancia en las interfaces no pueden tener inicializadores. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' El método "UnmanagedCallersOnly" "{0}" no puede implementar el miembro de interfaz "{1}" en el tipo "{2}" diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 08ac676ffe3a8..7bfd4559e267a 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -287,6 +287,11 @@ L'attribut AsyncMethodBuilder n'est pas autorisé pour les méthodes anonymes sans type de retour explicite. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} La chaîne d’entrée ne peut pas être convertie en représentation d’octet UTF-8 équivalente. {0} @@ -832,6 +837,46 @@ Les propriétés d'instance dans les interfaces ne peuvent pas avoir d'initialiseurs. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' La méthode UnmanagedCallersOnly '{0}' ne peut pas implémenter le membre d'interface '{1}' dans le type '{2}' diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 38fef3982c353..ca414bae0746d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -287,6 +287,11 @@ L'attributo AsyncMethodBuilder non è consentito in metodi anonimi senza un tipo restituito esplicito. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Impossibile convertire la stringa di input nella rappresentazione di UTF-8 byte equivalente. {0} @@ -832,6 +837,46 @@ Le proprietà di istanza nelle interfacce non possono avere inizializzatori. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' Il metodo '{0}' di 'UnmanagedCallersOnly' non può implementare il membro di interfaccia '{1}' nel tipo '{2}' diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index f9942438af4d6..d81d2cfdfed4e 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -287,6 +287,11 @@ AsyncMethodBuilder 属性は、明示的な戻り値の型のない匿名メソッドでは許可されていません。 + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 入力文字列を同等の UTF-8 バイト表現に変換できません。{0} @@ -832,6 +837,46 @@ インターフェイス内のインスタンス プロパティは初期化子を持つことができません。 + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' 'UnmanagedCallersOnly' メソッド '{0}' は、インターフェイス メンバー '{1}' を型 '{2}' で実装できません diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index 641230af2be82..b4d71e4264815 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -287,6 +287,11 @@ AsyncMethodBuilder 특성은 명시적 반환 형식이 없는 익명 메서드에서 허용되지 않습니다. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 입력 문자열은 해당 UTF-8 바이트 표현으로 변환할 수 없습니다. {0} @@ -832,6 +837,46 @@ 인터페이스의 인스턴스 속성은 이니셜라이저를 사용할 수 없습니다. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' 'UnmanagedCallersOnly' 메서드 '{0}'은(는) '{2}' 유형의 인터페이스 멤버 '{1}'을(를) 구현할 수 없습니다. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index ab40cb7d47d42..baaf4515d83eb 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -287,6 +287,11 @@ Atrybut AsyncMethodBuilder jest niedozwolony w metodach anonimowych bez jawnego zwracanego typu. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Nie można przekonwertować ciągu wejściowego na równoważną reprezentację bajtową UTF-8. {0} @@ -832,6 +837,46 @@ Właściwości wystąpienia w interfejsach nie mogą mieć inicjatorów. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' Metoda "UnmanagedCallersOnly" "{0}" nie może implementować składowej interfejsu "{1}" w typie "{2}" diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 26e0564ba92cf..fc6fd52125ea4 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -287,6 +287,11 @@ O atributo AsyncMethodBuilder não é permitido em métodos anônimos sem um tipo de retorno explícito. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} A cadeia de caracteres de entrada não pode ser convertida na representação equivalente em bytes UTF-8. {0} @@ -832,6 +837,46 @@ As propriedades da instância nas interfaces não podem ter inicializadores. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' O método 'UnmanagedCallersOnly' '{0}' não pode implementar o membro de interface '{1}' no tipo '{2}' diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index 60b79370b77a0..5211def99a97c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -287,6 +287,11 @@ Атрибут AsyncMethodBuilder запрещен для анонимных методов без явного типа возвращаемого значения. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Невозможно преобразовать входную строку в эквивалентное байтовое представление UTF-8.{0} @@ -832,6 +837,46 @@ Свойства экземпляра в интерфейсах не могут иметь инициализаторы. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' Метод UnmanagedCallersOnly "{0}" не может реализовать элемент интерфейса "{1}" в типе "{2}" diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 37e30b249392a..658484af57d45 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -287,6 +287,11 @@ Açık dönüş türü olmadan, anonim yöntemlerde AsyncMethodBuilder özniteliğine izin verilmez. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Giriş dizesi eşdeğer UTF-8 bayt gösterimine dönüştürülemiyor. {0} @@ -832,6 +837,46 @@ Arabirimlerdeki örnek özelliklerinin başlatıcıları olamaz. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' '{0}' 'UnmanagedCallersOnly' yöntemi, '{1}' arabirim üyesini '{2}' türünde uygulayamaz diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 98a1fd566f8c9..046d4e8d8eaa5 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -287,6 +287,11 @@ 没有显式返回类型的匿名方法不允许使用 AsyncMethodBuilder 属性。 + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 无法将输入字符串转换为等效的 UTF-8 字节表示形式。 {0} @@ -832,6 +837,46 @@ 接口中的实例属性不能具有初始值设定项。 + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' “UnmanagedCallersOnly”方法“{0}”无法实现类型“{2}”中的接口成员“{1}” diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index 524c120404463..7f5812d8822dc 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -287,6 +287,11 @@ 沒有明確傳回型別的匿名方法上不允許 AsyncMethodBuilder 屬性。 + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 輸入字串無法轉換成對等的 UTF-8 位元組表示法。{0} @@ -832,6 +837,46 @@ 介面中的執行個體屬性不可有初始設定式。 + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. + + + + 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}'. + + + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. + Cannot intercept: compilation does not contain a file with path '{0}'. + + + + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + + + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + + + + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + 'UnmanagedCallersOnly' method '{0}' cannot implement interface member '{1}' in type '{2}' 'UnmanagedCallersOnly' 方法 '{0}' 無法在類型 '{2}' 中實作介面成員 '{1}' diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 46c92f48719f7..611911cc64a48 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -184,4 +184,398 @@ static class D var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call siteinterceptable call site"); verifier.VerifyDiagnostics(); } + + [Fact] + public void CallNotInterceptable() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 + { + public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27000: Cannot intercept a call to 'C.InterceptableMethod(string)' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + // [InterceptsLocation("Program.cs", 14, 10)] + Diagnostic(ErrorCode.ERR_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(21, 6)); + } + + [Fact] + public void InterceptorCannotBeGeneric_01() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 + { + [Interceptable] + public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 10)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(22,6): error CS27001: Method 'D.Interceptor1(I1, string)' cannot be used as an interceptor because it or its containing type has type parameters. + // [InterceptsLocation("Program.cs", 15, 10)] + Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 15, 10)").WithArguments("D.Interceptor1(I1, string)").WithLocation(22, 6)); + } + + [Fact] + public void InterceptorCannotBeGeneric_02() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 + { + [Interceptable] + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + } + + static class Program + { + public static void Main() + { + C.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + public static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27001: Method 'D.Interceptor1(string)' cannot be used as an interceptor because it or its containing type has type parameters. + // [InterceptsLocation("Program.cs", 14, 10)] + Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("D.Interceptor1(string)").WithLocation(21, 6)); + } + + [Fact] + public void InterceptorCannotBeGeneric_03() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 + { + [Interceptable] + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + } + + static class Program + { + public static void Main() + { + C.InterceptableMethod("call site"); + } + } + + static class Outer + { + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + public static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(23,10): error CS27001: Method 'Outer.D.Interceptor1(string)' cannot be used as an interceptor because it or its containing type has type parameters. + // [InterceptsLocation("Program.cs", 14, 10)] + Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("Outer.D.Interceptor1(string)").WithLocation(23, 10) + ); + } + + [Fact] + public void InterceptsLocationBadAttributeArguments_01() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + static class D + { + [InterceptsLocation("Program.cs", 1, "10")] + [InterceptsLocation("Program.cs", 1, 1, 9999)] + [InterceptsLocation("Program.cs", ERROR, 1)] + [InterceptsLocation()] + public static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(6,42): error CS1503: Argument 3: cannot convert from 'string' to 'int' + // [InterceptsLocation("Program.cs", 1, "10")] + Diagnostic(ErrorCode.ERR_BadArgType, @"""10""").WithArguments("3", "string", "int").WithLocation(6, 42), + // Program.cs(7,6): error CS1729: 'InterceptsLocationAttribute' does not contain a constructor that takes 4 arguments + // [InterceptsLocation("Program.cs", 1, 1, 9999)] + Diagnostic(ErrorCode.ERR_BadCtorArgCount, @"InterceptsLocation(""Program.cs"", 1, 1, 9999)").WithArguments("System.Runtime.CompilerServices.InterceptsLocationAttribute", "4").WithLocation(7, 6), + // Program.cs(8,39): error CS0103: The name 'ERROR' does not exist in the current context + // [InterceptsLocation("Program.cs", ERROR, 1)] + Diagnostic(ErrorCode.ERR_NameNotInContext, "ERROR").WithArguments("ERROR").WithLocation(8, 39), + // Program.cs(9,6): error CS7036: There is no argument given that corresponds to the required parameter 'filePath' of 'InterceptsLocationAttribute.InterceptsLocationAttribute(string, int, int)' + // [InterceptsLocation()] + Diagnostic(ErrorCode.ERR_NoCorrespondingArgument, "InterceptsLocation()").WithArguments("filePath", "System.Runtime.CompilerServices.InterceptsLocationAttribute.InterceptsLocationAttribute(string, int, int)").WithLocation(9, 6) + ); + } + + [Fact] + public void InterceptsLocationBadPath_01() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("BAD", 14, 10)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27002: Cannot intercept: compilation does not contain a file with path 'BAD'. + // [InterceptsLocation("BAD", 14, 10)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"InterceptsLocation(""BAD"", 14, 10)").WithArguments("BAD").WithLocation(21, 6) + ); + } + + [Fact] + public void InterceptsLocationBadPath_02() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("projects/Program.cs", 14, 10)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "/Users/me/projects/Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // /Users/me/projects/Program.cs(21,6): error CS27003: Cannot intercept: compilation does not contain a file with path 'projects/Program.cs'. Did you mean to use path '/Users/me/projects/Program.cs'? + // [InterceptsLocation("projects/Program.cs", 14, 10)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"InterceptsLocation(""projects/Program.cs"", 14, 10)").WithArguments("projects/Program.cs", "/Users/me/projects/Program.cs").WithLocation(21, 6) + ); + } + + [Fact] + public void InterceptsLocationBadPosition_01() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 100, 1)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27005: The given file has '23' lines, which is fewer than the provided line number '100'. + // [InterceptsLocation("Program.cs", 100, 1)] + Diagnostic(ErrorCode.ERR_InterceptorLineOutOfRange, @"InterceptsLocation(""Program.cs"", 100, 1)").WithArguments("23", "100").WithLocation(21, 6) + ); + } + + [Fact] + public void InterceptsLocationBadPosition_02() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 1000)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27006: The given line is '23' characters long, which is fewer than the provided character number '1000'. + // [InterceptsLocation("Program.cs", 15, 1000)] + Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 15, 1000)").WithArguments("23", "1000").WithLocation(21, 6) + ); + } + + [Fact] + public void InterceptsLocationBadPosition_03() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 8)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable call, but rather to token 'c'. + // [InterceptsLocation("Program.cs", 14, 8)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 8)").WithArguments("c").WithLocation(21, 6) + ); + } + + [Fact] + public void SignatureMismatch_01() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 8)] + public static I1 Interceptor1(this I1 i1, int param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable call, but rather to token 'c'. + // [InterceptsLocation("Program.cs", 14, 8)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 8)").WithArguments("c").WithLocation(21, 6) + ); + } } \ No newline at end of file diff --git a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs index a2cea39d04673..9ca8259fb077c 100644 --- a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs +++ b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs @@ -43,7 +43,8 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer "CS1969", // ErrorCode.ERR_DynamicRequiredTypesMissing: "CS8984", // ErrorCode.ERR_EncUpdateFailedDelegateTypeChanged: "CS9026", // ErrorCode.ERR_CannotBeConvertedToUtf8: - "CS9068" // ErrorCode.ERR_FileTypeNonUniquePath: + "CS9068", // ErrorCode.ERR_FileTypeNonUniquePath: + "CS27000" // ErrorCode.ERR_CallNotInterceptable: )] internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics { From 0a633b978a82fa100ea401fc435637a2783149d6 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 21 Mar 2023 15:38:29 -0700 Subject: [PATCH 05/49] more --- src/Compilers/CSharp/Portable/CSharpResources.resx | 2 +- .../Symbols/Source/SourceMethodSymbolWithAttributes.cs | 1 + .../CSharp/Portable/xlf/CSharpResources.cs.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.de.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.es.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.fr.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.it.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.ja.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.ko.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.pl.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.pt-BR.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.ru.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.tr.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf | 4 ++-- .../CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf | 4 ++-- .../Test/Semantic/Semantics/InterceptorsTests.cs | 10 +++++----- 16 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index a4a90d17a08c6..f616ed86582b5 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7527,7 +7527,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index fd009452110f2..7885049d772e4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -948,6 +948,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments { Kind: not TypedConstantKind.Array, Value: int lineNumber }, { Kind: not TypedConstantKind.Array, Value: int characterNumber }]) { + // Since the attribute does not have errors (asserted above), it should be guaranteed that we have the above arguments. throw ExceptionUtilities.Unreachable(); } diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index f83ec6d482c1b..07a28aa65e334 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 320e0830788ab..cf0912018b6f4 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 81790cb569381..ad85de4608499 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 011fc4516fbc2..5c0013fa7121e 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 0cbc7ea559582..4ba031569de4e 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index e0edafece1b72..d4cecd422cc3f 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index caf20c076bf47..ac6ef7df6652d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index 5dabcf8180738..cf044db461d68 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index f227dd2860119..11771202005e1 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index 9ad17160979d2..2627e67f74e97 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 8b8b46522f589..fa53cada0c007 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 5431f1af292bd..86ce652b01a63 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index 7abe478d58371..c523880cfefba 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -873,8 +873,8 @@ - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable call, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 611911cc64a48..f4f041ec15f19 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -537,7 +537,7 @@ static class D """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable call, but rather to token 'c'. + // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable method, but rather to token 'c'. // [InterceptsLocation("Program.cs", 14, 8)] Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 8)").WithArguments("c").WithLocation(21, 6) ); @@ -567,15 +567,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 8)] + [InterceptsLocation("Program.cs", 14, 10)] public static I1 Interceptor1(this I1 i1, int param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable call, but rather to token 'c'. - // [InterceptsLocation("Program.cs", 14, 8)] - Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 8)").WithArguments("c").WithLocation(21, 6) + // Program.cs(15,11): error CS27007: Cannot intercept method 'Program.InterceptableMethod(I1, string)' with interceptor 'D.Interceptor1(I1, int)' because the signatures do not match. + // c.InterceptableMethod("call site"); + Diagnostic(ErrorCode.ERR_InterceptorSignatureMismatch, "InterceptableMethod").WithArguments("Program.InterceptableMethod(I1, string)", "D.Interceptor1(I1, int)").WithLocation(15, 11) ); } } \ No newline at end of file From 380a36fbd335733132f8b68f82e0902f33e54452 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 21 Mar 2023 17:09:06 -0700 Subject: [PATCH 06/49] more --- .../CSharp/Portable/CSharpResources.resx | 6 + .../Portable/Compilation/CSharpCompilation.cs | 5 +- .../CSharp/Portable/Errors/ErrorCode.cs | 2 + .../CSharp/Portable/Errors/ErrorFacts.cs | 4 + .../LocalRewriter/LocalRewriter_Call.cs | 16 ++- .../MethodWellKnownAttributeData.cs | 17 --- .../Symbols/MemberSignatureComparer.cs | 2 +- .../Symbols/ReducedExtensionMethodSymbol.cs | 1 - .../Symbols/SignatureOnlyMethodSymbol.cs | 1 - .../SourceMethodSymbolWithAttributes.cs | 14 ++- .../Symbols/Wrapped/WrappedMethodSymbol.cs | 1 - .../Portable/xlf/CSharpResources.cs.xlf | 10 ++ .../Portable/xlf/CSharpResources.de.xlf | 10 ++ .../Portable/xlf/CSharpResources.es.xlf | 10 ++ .../Portable/xlf/CSharpResources.fr.xlf | 10 ++ .../Portable/xlf/CSharpResources.it.xlf | 10 ++ .../Portable/xlf/CSharpResources.ja.xlf | 10 ++ .../Portable/xlf/CSharpResources.ko.xlf | 10 ++ .../Portable/xlf/CSharpResources.pl.xlf | 10 ++ .../Portable/xlf/CSharpResources.pt-BR.xlf | 10 ++ .../Portable/xlf/CSharpResources.ru.xlf | 10 ++ .../Portable/xlf/CSharpResources.tr.xlf | 10 ++ .../Portable/xlf/CSharpResources.zh-Hans.xlf | 10 ++ .../Portable/xlf/CSharpResources.zh-Hant.xlf | 10 ++ .../Semantic/Semantics/InterceptorsTests.cs | 109 ++++++++++++++++++ .../Test/Syntax/Diagnostics/DiagnosticTest.cs | 2 + .../CSharpLspBuildOnlyDiagnostics.cs | 3 +- 27 files changed, 277 insertions(+), 36 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index f616ed86582b5..db14d5be63576 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7535,4 +7535,10 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. + + An interceptable method must be an ordinary member method. + + + An interceptor method must be an ordinary member method. + \ 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 5d7b8025a8689..f0b187884c21e 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -2253,7 +2253,6 @@ internal void AddInterception(InterceptsLocationAttributeData location, MethodSy { Debug.Assert(!_declarationDiagnosticsFrozen); LazyInitializer.EnsureInitialized(ref _interceptions).Add((location, interceptor)); - // PROTOTYPE(ic): when all this is done, build a map and give duplicate/misplaced diagnostics which we can use in lowering. } internal (InterceptsLocationAttributeData data, MethodSymbol interceptor)? GetInterceptor(Location? callLocation) @@ -2277,10 +2276,10 @@ internal void AddInterception(InterceptsLocationAttributeData location, MethodSy return null; } - internal void BuildInterceptorsMap() + private void BuildInterceptorsMap() { // PROTOTYPE(ic): build a map where we can quickly lookup with a location and get a symbol. - // At this time, should report any duplicate interceptor diagnostics. + // At this time, should report any duplicate interception diagnostics. // NB: the attribute which appears lexically first wins a tie. Subsequent attributes referring to same location result in errors. } diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 36b615a7af335..eb8223fb9b358 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2199,6 +2199,8 @@ internal enum ErrorCode ERR_InterceptorLineOutOfRange = 27005, ERR_InterceptorCharacterOutOfRange = 27006, ERR_InterceptorSignatureMismatch = 27007, + ERR_InterceptableMethodMustBeOrdinary = 27008, + ERR_InterceptorMethodMustBeOrdinary = 27009, #endregion // Note: you will need to do the following after adding warnings: diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 47c96968a19c2..51b349bfde2bf 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -580,6 +580,7 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_CannotBeConvertedToUtf8: case ErrorCode.ERR_FileTypeNonUniquePath: case ErrorCode.ERR_CallNotInterceptable: + case ErrorCode.ERR_InterceptorSignatureMismatch: // Update src\EditorFeatures\CSharp\LanguageServer\CSharpLspBuildOnlyDiagnostics.cs // whenever new values are added here. return true; @@ -2313,8 +2314,11 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_InterceptorCannotBeGeneric: case ErrorCode.ERR_InterceptorPathNotInCompilation: case ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate: + case ErrorCode.ERR_InterceptorPositionBadToken: case ErrorCode.ERR_InterceptorLineOutOfRange: case ErrorCode.ERR_InterceptorCharacterOutOfRange: + case ErrorCode.ERR_InterceptableMethodMustBeOrdinary: + case ErrorCode.ERR_InterceptorMethodMustBeOrdinary: return false; default: // NOTE: All error codes must be explicitly handled in this switch statement diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index ad9f01e678c4a..8a7e2a8f84661 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -152,26 +152,24 @@ bool invokedAsExtensionMethod var interceptableLocation = node.InterceptableLocation; if (this._compilation.GetInterceptor(interceptableLocation) is not var (interceptsLocationAttributeData, interceptor)) { - // PROTOTYPE(ic): if an interceptor doesn't refer to anything, we want to report it. - // that means we need some way of flagging when an interceptor is used. + // The call was not intercepted. return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); } Debug.Assert(interceptableLocation != null); if (!method.IsInterceptable) { + // PROTOTYPE(ic): it was speculated that we could avoid work if we know the current method is not interceptable. + // i.e. use this as an early out before even calling Compilation.GetInterceptor. + // But by calling 'GetInterceptor' before this, we don't really avoid that work. Is that fine? this._diagnostics.Add(ErrorCode.ERR_CallNotInterceptable, interceptsLocationAttributeData.AttributeLocation, method); return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); } - if (interceptor.Arity != 0 || interceptor.ContainingType.VisitType(static (type, _, _) => type.GetArity() != 0, arg: (object?)null) is not null) - { - // PROTOTYPE(ic): for now, let's disallow type arguments on the method or containing types. - // eventually, we could consider doing a type argument inference. - this._diagnostics.Add(ErrorCode.ERR_ModuleEmitFailure, interceptableLocation); - return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); - } + Debug.Assert(interceptor.Arity == 0); + // When the original call is to an instance method, and the interceptor is an extension method, + // we need to take special care to intercept with the extension method as though it is being called in reduced form. var needToReduce = receiverOpt != null && interceptor.IsExtensionMethod; var symbolForCompare = needToReduce ? ReducedExtensionMethodSymbol.Create(interceptor, receiverOpt!.Type, _compilation) : interceptor; if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare)) diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs index 6d7d2b18e3929..f2a1894a2df53 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs @@ -165,22 +165,5 @@ public bool HasInterceptableAttribute SetDataStored(); } } - - // PROTOTYPE(ic): do we want to expose InterceptsLocation data on the symbol itself? - // private ImmutableArray _interceptsLocations = ImmutableArray.Empty; - - // internal void AddInterceptsLocation(InterceptsLocationAttributeData interceptsLocation) - // { - // _interceptsLocations = _interceptsLocations.Add(interceptsLocation); - // } - - // public ImmutableArray InterceptsLocationsAttributeData - // { - // get - // { - // VerifySealed(expected: true); - // return _interceptsLocations; - // } - // } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs b/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs index edf4daf768a64..ece99c14d634c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs @@ -369,7 +369,7 @@ private MemberSignatureComparer( bool considerTypeConstraints, bool considerCallingConvention, bool considerRefKindDifferences, - bool considerArity = true, // PROTOTYPE(ic): make parameter required? + bool considerArity = true, // PROTOTYPE(ic): remove default value? TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers) { Debug.Assert(!considerExplicitlyImplementedInterfaces || considerName, "Doesn't make sense to consider interfaces separately from name."); diff --git a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs index cc038a794fa05..ea3dc1077c72c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs @@ -574,7 +574,6 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l internal override bool IsNullableAnalysisEnabled() => throw ExceptionUtilities.Unreachable(); - // PROTOTYPE(ic): These symbols are only used for public API and shouldn't be able to call into this internal override bool IsInterceptable => throw ExceptionUtilities.Unreachable(); public override bool Equals(Symbol obj, TypeCompareKind compareKind) diff --git a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs index d75ef7071bcf9..676590e63402c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs @@ -96,7 +96,6 @@ public SignatureOnlyMethodSymbol( internal sealed override bool IsNullableAnalysisEnabled() => throw ExceptionUtilities.Unreachable(); - // PROTOTYPE(ic) internal sealed override bool IsInterceptable => throw ExceptionUtilities.Unreachable(); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 7885049d772e4..64347819bdc4b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -594,6 +594,11 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut } else if (attribute.IsTargetAttribute(this, AttributeDescription.InterceptableAttribute)) { + if (MethodKind != MethodKind.Ordinary) + { + // PROTOTYPE(ic): consider relaxing this in future. + diagnostics.Add(ErrorCode.ERR_InterceptableMethodMustBeOrdinary, arguments.AttributeSyntaxOpt.Location); + } arguments.GetOrCreateData().HasInterceptableAttribute = true; } else if (attribute.IsTargetAttribute(this, AttributeDescription.InterceptsLocationAttribute)) @@ -963,6 +968,13 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } + if (MethodKind != MethodKind.Ordinary) + { + // PROTOTYPE(ic): consider relaxing this in future. + diagnostics.Add(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, attributeLocation); + return; + } + var syntaxTrees = DeclaringCompilation.SyntaxTrees; var matchingTree = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath == filePath, filePath); if (matchingTree == null) @@ -996,8 +1008,6 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } - // PROTOTYPE(ic): the call-site binding is going to need to look some of this stuff up again. - // are we sure this is the best place to do these kinds of checks? var referencedPosition = line.Start + characterNumber; var root = matchingTree.GetRoot(); var referencedToken = root.FindToken(referencedPosition); diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs index 8d673f871233a..3e463e1ed9982 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs @@ -186,7 +186,6 @@ public override bool IsImplicitlyDeclared } } - // PROTOTYPE(ic): sealed? internal override bool IsInterceptable { get diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 07a28aa65e334..97811b558c989 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -842,6 +842,11 @@ Vlastnosti instance v rozhraních nemůžou mít inicializátory. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index cf0912018b6f4..ca976dcc759b4 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -842,6 +842,11 @@ Instanzeigenschaften in Schnittstellen können keine Initialisierer aufweisen. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index ad85de4608499..9f07f8b1edc7a 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -842,6 +842,11 @@ Las propiedades de la instancia en las interfaces no pueden tener inicializadores. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 5c0013fa7121e..821544c0c0925 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -842,6 +842,11 @@ Les propriétés d'instance dans les interfaces ne peuvent pas avoir d'initialiseurs. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 4ba031569de4e..d37a399057ddf 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -842,6 +842,11 @@ Le proprietà di istanza nelle interfacce non possono avere inizializzatori. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index d4cecd422cc3f..efcff273b51d2 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -842,6 +842,11 @@ インターフェイス内のインスタンス プロパティは初期化子を持つことができません。 + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index ac6ef7df6652d..a29ef49263f9b 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -842,6 +842,11 @@ 인터페이스의 인스턴스 속성은 이니셜라이저를 사용할 수 없습니다. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index cf044db461d68..ebc6d0a6dcea5 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -842,6 +842,11 @@ Właściwości wystąpienia w interfejsach nie mogą mieć inicjatorów. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 11771202005e1..70c2ec380de75 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -842,6 +842,11 @@ As propriedades da instância nas interfaces não podem ter inicializadores. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index 2627e67f74e97..d6285337aaaca 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -842,6 +842,11 @@ Свойства экземпляра в интерфейсах не могут иметь инициализаторы. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index fa53cada0c007..90cc9ee0e9fb6 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -842,6 +842,11 @@ Arabirimlerdeki örnek özelliklerinin başlatıcıları olamaz. + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 86ce652b01a63..ae7be3fff37ae 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -842,6 +842,11 @@ 接口中的实例属性不能具有初始值设定项。 + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index c523880cfefba..dad0a422a84bb 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -842,6 +842,11 @@ 介面中的執行個體屬性不可有初始設定式。 + + An interceptable method must be an ordinary member method. + An interceptable method must be an ordinary member method. + + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -857,6 +862,11 @@ The given file has '{0}' lines, which is fewer than the provided line number '{1}'. + + An interceptor method must be an ordinary member method. + An interceptor method must be an ordinary member method. + + The provided character number must refer to the start of the token. Consider using character number '{0}' instead. The provided character number must refer to the start of the token. Consider using character number '{0}' instead. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index f4f041ec15f19..ccdbec2e08db3 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -118,6 +118,7 @@ static class D // PROTOTYPE(ic): test a case where the original method has type parameter constraints. // PROTOTYPE(ic): for now we will just completely disallow type parameters in the interceptor. + // PROTOTYPE(ic): test where interceptable is a constructed method or a retargeting method. [Fact] public void InterceptableInstanceMethod_InterceptorExtensionMethod() @@ -185,6 +186,114 @@ static class D verifier.VerifyDiagnostics(); } + [Fact] + public void InterceptableFromMetadata() + { + var source1 = """ + using System.Runtime.CompilerServices; + using System; + + public interface I1 { } + public class C : I1 + { + [Interceptable] + public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + } + """; + + var source2 = """ + using System.Runtime.CompilerServices; + using System; + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 8, 10)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + + var comp1 = CreateCompilation(new[] { (source1, "File1.cs"), s_attributesSource }); + comp1.VerifyEmitDiagnostics(); + + var verifier = CompileAndVerify((source2, "Program.cs"), references: new[] { comp1.ToMetadataReference() }, expectedOutput: "interceptor call site"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void LocalFunctionInterceptor() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 { } + + static class Program + { + [Interceptable] + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + + public static void Main() + { + InterceptableMethod("call site"); + + [InterceptsLocation("Program.cs", 13, 8)] + static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyDiagnostics( + // Program.cs(16,10): error CS27009: An interceptor method must be an ordinary member method. + // [InterceptsLocation("Program.cs", 13, 8)] + Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, @"InterceptsLocation(""Program.cs"", 13, 8)").WithLocation(16, 10), + // Program.cs(17,21): warning CS8321: The local function 'Interceptor1' is declared but never used + // static void Interceptor1(string param) { Console.Write("interceptor " + param); } + Diagnostic(ErrorCode.WRN_UnreferencedLocalFunction, "Interceptor1").WithArguments("Interceptor1").WithLocation(17, 21) + ); + } + + [Fact] + public void LocalFunctionInterceptable() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 { } + + static class Program + { + public static void Main() + { + InterceptableMethod("call site"); + + [Interceptable] + static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + } + + [InterceptsLocation("Program.cs", 10, 8)] + public static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyDiagnostics( + // Program.cs(13,10): error CS27008: An interceptable method must be an ordinary member method. + // [Interceptable] + Diagnostic(ErrorCode.ERR_InterceptableMethodMustBeOrdinary, "Interceptable").WithLocation(13, 10)); + } + [Fact] public void CallNotInterceptable() { diff --git a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs index 3eddc361b3951..ccc46384a60f2 100644 --- a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs +++ b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs @@ -2937,6 +2937,8 @@ public void TestIsBuildOnlyDiagnostic() case ErrorCode.ERR_EncUpdateFailedDelegateTypeChanged: case ErrorCode.ERR_CannotBeConvertedToUtf8: case ErrorCode.ERR_FileTypeNonUniquePath: + case ErrorCode.ERR_CallNotInterceptable: + case ErrorCode.ERR_InterceptorSignatureMismatch: Assert.True(isBuildOnly, $"Check failed for ErrorCode.{errorCode}"); break; diff --git a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs index 9ca8259fb077c..f23cfa65e9abb 100644 --- a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs +++ b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs @@ -44,7 +44,8 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer "CS8984", // ErrorCode.ERR_EncUpdateFailedDelegateTypeChanged: "CS9026", // ErrorCode.ERR_CannotBeConvertedToUtf8: "CS9068", // ErrorCode.ERR_FileTypeNonUniquePath: - "CS27000" // ErrorCode.ERR_CallNotInterceptable: + "CS27000", // ErrorCode.ERR_CallNotInterceptable: + "CS27007" // ErrorCode.ERR_InterceptorSignatureMismatch )] internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics { From 83572ce80196669c33482801c8842bb294493578 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 21 Mar 2023 18:01:59 -0700 Subject: [PATCH 07/49] Implement "must refer to start of token" diagnostic --- .../CSharp/Portable/CSharpResources.resx | 2 +- .../CSharp/Portable/Errors/ErrorCode.cs | 1 + .../SourceMethodSymbolWithAttributes.cs | 10 ++++++ .../Portable/xlf/CSharpResources.cs.xlf | 4 +-- .../Portable/xlf/CSharpResources.de.xlf | 4 +-- .../Portable/xlf/CSharpResources.es.xlf | 4 +-- .../Portable/xlf/CSharpResources.fr.xlf | 4 +-- .../Portable/xlf/CSharpResources.it.xlf | 4 +-- .../Portable/xlf/CSharpResources.ja.xlf | 4 +-- .../Portable/xlf/CSharpResources.ko.xlf | 4 +-- .../Portable/xlf/CSharpResources.pl.xlf | 4 +-- .../Portable/xlf/CSharpResources.pt-BR.xlf | 4 +-- .../Portable/xlf/CSharpResources.ru.xlf | 4 +-- .../Portable/xlf/CSharpResources.tr.xlf | 4 +-- .../Portable/xlf/CSharpResources.zh-Hans.xlf | 4 +-- .../Portable/xlf/CSharpResources.zh-Hant.xlf | 4 +-- .../Semantic/Semantics/InterceptorsTests.cs | 36 +++++++++++++++++++ 17 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index db14d5be63576..0bc5b08072256 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7530,7 +7530,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. Cannot intercept method '{0}' with interceptor '{1}' because the signatures do not match. diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index eb8223fb9b358..abcd5b278d9fb 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2201,6 +2201,7 @@ internal enum ErrorCode ERR_InterceptorSignatureMismatch = 27007, ERR_InterceptableMethodMustBeOrdinary = 27008, ERR_InterceptorMethodMustBeOrdinary = 27009, + ERR_InterceptorMustReferToStartOfTokenPosition = 27010, #endregion // Note: you will need to do the following after adding warnings: diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 64347819bdc4b..b3286d8490dea 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -1022,6 +1022,16 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } + // Did they actually refer to the start of the token, not the middle? + var tokenPositionDifference = referencedPosition - referencedToken.Span.Start; + Debug.Assert(tokenPositionDifference >= 0); // if the referenced position were smaller than the token's start position, we wouldn't have matched it. + if (tokenPositionDifference != 0) + { + // tokens don't span multiple lines, so we can apply the difference we found to the characterNumber within the line and figure out which character the user should have used. + diagnostics.Add(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, attributeLocation, referencedToken.Text, characterNumber - tokenPositionDifference); + return; + } + DeclaringCompilation.AddInterception(new InterceptsLocationAttributeData(filePath, lineNumber, characterNumber, attributeLocation), this); } diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 97811b558c989..938cb58bd1cd8 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index ca976dcc759b4..fc4689808f9f5 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 9f07f8b1edc7a..496829a684b7a 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 821544c0c0925..3c66ff8489f00 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index d37a399057ddf..c74769854cf14 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index efcff273b51d2..3b6803fbe4a88 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index a29ef49263f9b..24f7fe6e66bcd 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index ebc6d0a6dcea5..cd98e266819fa 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 70c2ec380de75..010083f71c0de 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index d6285337aaaca..bbebe91686c63 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 90cc9ee0e9fb6..572ab7112340d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index ae7be3fff37ae..32bd1ea72906f 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index dad0a422a84bb..fff8ef1760ce0 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -868,8 +868,8 @@ - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. - The provided character number must refer to the start of the token. Consider using character number '{0}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index ccdbec2e08db3..e72eca0af8cea 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -652,6 +652,42 @@ static class D ); } + [Fact] + public void InterceptsLocationBadPosition_04() + { + 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(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 12)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '10' instead. + // [InterceptsLocation("Program.cs", 14, 12)] + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 14, 12)").WithArguments("InterceptableMethod", "10").WithLocation(21, 6) + ); + } + [Fact] public void SignatureMismatch_01() { From 8a27e7e0c02e7516835b8f6bf9679aa7cb8e7233 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 21 Mar 2023 18:30:05 -0700 Subject: [PATCH 08/49] fix build breaks in EE --- .../CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs | 2 ++ .../ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs index b2ad97f1b5419..f5425d2141ad3 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs @@ -451,6 +451,8 @@ public override bool IsExtern internal override bool IsInitOnly => false; + internal override bool IsInterceptable => false; + internal override ObsoleteAttributeData ObsoleteAttributeData { get { throw ExceptionUtilities.Unreachable(); } diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs index 20372abe82bdd..c3bc327c971c6 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs @@ -277,6 +277,8 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l internal override bool IsNullableAnalysisEnabled() => false; + internal override bool IsInterceptable => false; + protected override bool HasSetsRequiredMembersImpl => throw ExceptionUtilities.Unreachable(); #if DEBUG From 677f332c24184882f53f80834a8deb6e8126b70c Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 22 Mar 2023 10:46:17 -0700 Subject: [PATCH 09/49] fix formatting --- .../CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs | 1 - .../Symbols/Source/SourceMethodSymbolWithAttributes.cs | 6 +++--- .../Synthesized/SynthesizedIntrinsicOperatorSymbol.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs index 676590e63402c..ebd1f6e3a72a7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs @@ -98,7 +98,6 @@ public SignatureOnlyMethodSymbol( internal sealed override bool IsInterceptable => throw ExceptionUtilities.Unreachable(); - #region Not used by MethodSignatureComparer internal override bool GenerateDebugInfo { get { throw ExceptionUtilities.Unreachable(); } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index b3286d8490dea..fb38f2cabc022 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -949,9 +949,9 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments Debug.Assert(!arguments.Attribute.HasErrors); var attributeArguments = arguments.Attribute.CommonConstructorArguments; if (attributeArguments is not [ - { Kind: not TypedConstantKind.Array, Value: string filePath }, - { Kind: not TypedConstantKind.Array, Value: int lineNumber }, - { Kind: not TypedConstantKind.Array, Value: int characterNumber }]) + { Kind: not TypedConstantKind.Array, Value: string filePath }, + { Kind: not TypedConstantKind.Array, Value: int lineNumber }, + { Kind: not TypedConstantKind.Array, Value: int characterNumber }]) { // Since the attribute does not have errors (asserted above), it should be guaranteed that we have the above arguments. throw ExceptionUtilities.Unreachable(); diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs index bf5faeca861fd..4d428a9bdcc7f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs @@ -78,7 +78,7 @@ public override bool IsImplicitlyDeclared return true; } } - internal override sealed bool IsInterceptable + internal sealed override bool IsInterceptable { get { From 70a2fb4f023613c3453f3b040bdebb82fbb44ef0 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 22 Mar 2023 11:22:06 -0700 Subject: [PATCH 10/49] Address some build failures --- src/Compilers/CSharp/Portable/CSharpResources.resx | 9 +++------ src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs | 1 + .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 4 ++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 0bc5b08072256..e9121876fac95 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -106,19 +106,16 @@ - - text/microsoft-resx + text/microsoft-resx 2.0 - System.Resources.ResXResourceReader, - System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, - System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 <null> diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 51b349bfde2bf..8dce8f550be37 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -2319,6 +2319,7 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_InterceptorCharacterOutOfRange: case ErrorCode.ERR_InterceptableMethodMustBeOrdinary: case ErrorCode.ERR_InterceptorMethodMustBeOrdinary: + case ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition: return false; default: // NOTE: All error codes must be explicitly handled in this switch statement diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index e72eca0af8cea..2a3cbd952cd1e 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -263,6 +263,10 @@ public static void Main() ); } + // PROTOTYPE(ic): duplicates + // PROTOTYPE(ic): intercept with instance method + // PROTOTYPE(ic): intercept with instance base method + [Fact] public void LocalFunctionInterceptable() { From af5c723738344a292cb94df8360565b7ce774fba Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 22 Mar 2023 12:51:32 -0700 Subject: [PATCH 11/49] Only warn when intercepted lacks InterceptableAttribute --- .../CSharp/Portable/CSharpResources.resx | 5 ++++- src/Compilers/CSharp/Portable/Errors/ErrorCode.cs | 2 +- .../CSharp/Portable/Errors/ErrorFacts.cs | 2 +- .../Portable/Generated/ErrorFacts.Generated.cs | 1 + .../Lowering/LocalRewriter/LocalRewriter_Call.cs | 7 ++++--- .../CSharp/Portable/xlf/CSharpResources.cs.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.de.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.es.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.fr.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.it.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.ja.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.ko.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.pl.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.pt-BR.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.ru.xlf | 15 ++++++++++----- .../CSharp/Portable/xlf/CSharpResources.tr.xlf | 15 ++++++++++----- .../Portable/xlf/CSharpResources.zh-Hans.xlf | 15 ++++++++++----- .../Portable/xlf/CSharpResources.zh-Hant.xlf | 15 ++++++++++----- .../Test/Semantic/Semantics/InterceptorsTests.cs | 9 +++++---- .../Test/Syntax/Diagnostics/DiagnosticTest.cs | 2 +- .../CSharpLspBuildOnlyDiagnostics.cs | 2 +- 21 files changed, 148 insertions(+), 77 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index e9121876fac95..055deb03196f9 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7505,9 +7505,12 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ A switch expression arm does not begin with a 'case' keyword. - + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index abcd5b278d9fb..99a3b6ab8209a 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2191,7 +2191,7 @@ internal enum ErrorCode ERR_BadCaseInSwitchArm = 9134, // PROTOTYPE(ic): pack errors - ERR_CallNotInterceptable = 27000, + WRN_CallNotInterceptable = 27000, ERR_InterceptorCannotBeGeneric = 27001, ERR_InterceptorPathNotInCompilation = 27002, ERR_InterceptorPathNotInCompilationWithCandidate = 27003, diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 8dce8f550be37..d6b7f5b5ecc04 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -579,7 +579,7 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_EncUpdateFailedDelegateTypeChanged: case ErrorCode.ERR_CannotBeConvertedToUtf8: case ErrorCode.ERR_FileTypeNonUniquePath: - case ErrorCode.ERR_CallNotInterceptable: + case ErrorCode.WRN_CallNotInterceptable: case ErrorCode.ERR_InterceptorSignatureMismatch: // Update src\EditorFeatures\CSharp\LanguageServer\CSharpLspBuildOnlyDiagnostics.cs // whenever new values are added here. diff --git a/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs b/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs index c1203a2275310..3f0b27ee459b2 100644 --- a/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs @@ -315,6 +315,7 @@ public static bool IsWarning(ErrorCode code) case ErrorCode.WRN_CapturedPrimaryConstructorParameterPassedToBase: case ErrorCode.WRN_UnreadPrimaryConstructorParameter: case ErrorCode.WRN_AddressOfInAsync: + case ErrorCode.WRN_CallNotInterceptable: return true; default: return false; diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 8a7e2a8f84661..12730ce146c6f 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -162,8 +162,9 @@ bool invokedAsExtensionMethod // PROTOTYPE(ic): it was speculated that we could avoid work if we know the current method is not interceptable. // i.e. use this as an early out before even calling Compilation.GetInterceptor. // But by calling 'GetInterceptor' before this, we don't really avoid that work. Is that fine? - this._diagnostics.Add(ErrorCode.ERR_CallNotInterceptable, interceptsLocationAttributeData.AttributeLocation, method); - return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + // PROTOTYPE(ic): eventually we probably want this to be an error but for now it's convenient to just warn + // so we can experiment with intercepting APIs that haven't yet been marked. + this._diagnostics.Add(ErrorCode.WRN_CallNotInterceptable, interceptsLocationAttributeData.AttributeLocation, method); } Debug.Assert(interceptor.Arity == 0); @@ -222,7 +223,7 @@ public override BoundNode VisitCall(BoundCall node) var (method, receiverOpt, arguments, argsToParamsOpt, argRefKindsOpt, invokedAsExtensionMethod) = InterceptCallAndAdjustArguments(node); // Rewrite the receiver - BoundExpression? rewrittenReceiver = VisitExpression(node.ReceiverOpt); + BoundExpression? rewrittenReceiver = VisitExpression(receiverOpt); ArrayBuilder? temps = null; var rewrittenArguments = VisitArgumentsAndCaptureReceiverIfNeeded( diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 938cb58bd1cd8..40b83a85a2d22 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -292,11 +292,6 @@ Atribut AsyncMethodBuilder je u anonymních metod bez explicitního návratového typu zakázaný. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Vstupní řetězec nelze převést na ekvivalentní reprezentaci bajtů UTF-8. {0} @@ -2127,6 +2122,16 @@ Tato kombinace argumentů parametru může vystavit proměnné, na které odkazuje parametr, mimo obor jejich deklarace + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. Atribut CallerArgumentExpressionAttribute použitý u parametru {0} nebude mít žádný účinek. Argument je použitý s neplatným názvem parametru. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index fc4689808f9f5..553fb710432ae 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -292,11 +292,6 @@ Das AsyncMethodBuilder-Attribut ist für anonyme Methoden ohne expliziten Rückgabetyp unzulässig. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Die Eingabezeichenfolge kann nicht in die entsprechende UTF-8-Byte-Darstellung konvertiert werden. {0} @@ -2127,6 +2122,16 @@ Diese Kombination aus Argumenten führt möglicherweise dazu, dass vom Parameter referenzierte Variablen außerhalb ihres Deklarationsbereichs verfügbar gemacht werden. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. Das CallerArgumentExpressionAttribute, das auf den Parameter „{0}“ angewendet wird, hat keine Auswirkungen. Es wird mit einem ungültigen Parameternamen angewendet. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 496829a684b7a..439a57843949e 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -292,11 +292,6 @@ El atributo AsyncMethodBuilder no se permite en métodos anónimos sin un tipo de valor devuelto explícito. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} La cadena de entrada no se puede convertir en la representación de bytes UTF-8 equivalente. {0} @@ -2127,6 +2122,16 @@ Esta combinación de argumentos puede exponer variables a las que el parámetro hace referencia fuera de su ámbito de declaración + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. El atributo CallerArgumentExpressionAttribute aplicado al parámetro "{0}" no tendrá ningún efecto. Se ha aplicado con un nombre de parámetro no válido. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 3c66ff8489f00..96631155ffa23 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -292,11 +292,6 @@ L'attribut AsyncMethodBuilder n'est pas autorisé pour les méthodes anonymes sans type de retour explicite. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} La chaîne d’entrée ne peut pas être convertie en représentation d’octet UTF-8 équivalente. {0} @@ -2127,6 +2122,16 @@ Cette combinaison d'arguments pour peut exposer les variables référencées par le paramètre en dehors de la portée de leur déclaration + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. Le CallerArgumentExpressionAttribute appliqué au paramètre « {0} » n’aura aucun effet. Il est appliqué avec un nom de paramètre non valide. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index c74769854cf14..d5308fdcf8672 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -292,11 +292,6 @@ L'attributo AsyncMethodBuilder non è consentito in metodi anonimi senza un tipo restituito esplicito. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Impossibile convertire la stringa di input nella rappresentazione di UTF-8 byte equivalente. {0} @@ -2127,6 +2122,16 @@ Questa combinazione di argomenti potrebbe esporre variabili a cui fa riferimento il parametro al di fuori del relativo ambito di dichiarazione + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. CallerArgumentExpressionAttribute applicato al parametro '{0}' non avrà alcun effetto. È applicato con un nome di parametro non valido. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index 3b6803fbe4a88..90443011ef819 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -292,11 +292,6 @@ AsyncMethodBuilder 属性は、明示的な戻り値の型のない匿名メソッドでは許可されていません。 - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 入力文字列を同等の UTF-8 バイト表現に変換できません。{0} @@ -2127,6 +2122,16 @@ この引数の組み合わせは、パラメーターによって参照される変数が宣言のスコープ外に公開される可能性があります + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. パラメーター '{0}' に適用された CallerArgumentExpressionAttribute は、無効なパラメーター名で適用されているため無効となります diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index 24f7fe6e66bcd..94ba498fea3e7 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -292,11 +292,6 @@ AsyncMethodBuilder 특성은 명시적 반환 형식이 없는 익명 메서드에서 허용되지 않습니다. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 입력 문자열은 해당 UTF-8 바이트 표현으로 변환할 수 없습니다. {0} @@ -2127,6 +2122,16 @@ 이 인수 조합은 선언 범위 외부의 매개 변수에서 참조하는 변수를 노출할 수 있습니다. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. 매개 변수 '{0}'에 적용된 CallerArgumentExpressionAttribute는 효과가 없습니다. 잘못된 매개 변수 이름으로 적용되었습니다. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index cd98e266819fa..842993f12691b 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -292,11 +292,6 @@ Atrybut AsyncMethodBuilder jest niedozwolony w metodach anonimowych bez jawnego zwracanego typu. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Nie można przekonwertować ciągu wejściowego na równoważną reprezentację bajtową UTF-8. {0} @@ -2127,6 +2122,16 @@ Ta kombinacja argumentów może uwidaczniać zmienne przywoływane przez parametr poza zakresem deklaracji + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. Atrybut CallerArgumentExpressionAttribute zastosowany do parametru "{0}" nie odniesie żadnego skutku. Zastosowano go z nieprawidłową nazwą parametru. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 010083f71c0de..7b8fc100725b4 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -292,11 +292,6 @@ O atributo AsyncMethodBuilder não é permitido em métodos anônimos sem um tipo de retorno explícito. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} A cadeia de caracteres de entrada não pode ser convertida na representação equivalente em bytes UTF-8. {0} @@ -2127,6 +2122,16 @@ Essa combinação de argumentos pode expor variáveis referenciadas por parâmetro fora de seu escopo de declaração + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. O CallerArgumentExpressionAttribute aplicado ao parâmetro '{0}' não terá efeito. Ele é aplicado com um nome de parâmetro inválido. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index bbebe91686c63..c8c009fe98d05 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -292,11 +292,6 @@ Атрибут AsyncMethodBuilder запрещен для анонимных методов без явного типа возвращаемого значения. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Невозможно преобразовать входную строку в эквивалентное байтовое представление UTF-8.{0} @@ -2127,6 +2122,16 @@ Эта комбинация аргументов может представить переменные, на которые ссылается параметр, за пределами области их объявления. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. Применение класса CallerArgumentExpressionAttribute к параметру "{0}" не подействует, поскольку он применен с недопустимым именем параметра. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 572ab7112340d..05ea47434742b 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -292,11 +292,6 @@ Açık dönüş türü olmadan, anonim yöntemlerde AsyncMethodBuilder özniteliğine izin verilmez. - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} Giriş dizesi eşdeğer UTF-8 bayt gösterimine dönüştürülemiyor. {0} @@ -2127,6 +2122,16 @@ Bu argüman kombinasyonu, parametre tarafından başvurulan değişkenleri bildirim kapsamı dışında gösterebilir. + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. '{0}' parametresine uygulanan CallerArgumentExpressionAttribute hiçbir etkiye sahip olmaz. Geçersiz bir parametre adıyla uygulanır. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 32bd1ea72906f..34318bb26edf3 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -292,11 +292,6 @@ 没有显式返回类型的匿名方法不允许使用 AsyncMethodBuilder 属性。 - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 无法将输入字符串转换为等效的 UTF-8 字节表示形式。 {0} @@ -2127,6 +2122,16 @@ 这种参数组合可能会在变量声明范围之外公开由参数引用的变量 + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. 应用于参数“{0}”的 CallerArgumentExpressionAttribute 将不起任何作用。它采用了无效的参数名。 diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index fff8ef1760ce0..231d15f8d64a1 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -292,11 +292,6 @@ 沒有明確傳回型別的匿名方法上不允許 AsyncMethodBuilder 屬性。 - - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - - The input string cannot be converted into the equivalent UTF-8 byte representation. {0} 輸入字串無法轉換成對等的 UTF-8 位元組表示法。{0} @@ -2127,6 +2122,16 @@ 此引數組合會在其宣告範圍外公開參數所參考的變數 + + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + + + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + + The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name. 套用到參數 '{0}' 的 CallerArgumentExpressionAttribute 將沒有效果。它套用了不正確的參數名稱。 diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 2a3cbd952cd1e..6edc6d3fc4678 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -328,9 +328,10 @@ static class D """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(21,6): error CS27000: Cannot intercept a call to 'C.InterceptableMethod(string)' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + // Program.cs(21,6): warning CS27000: Cannot intercept a call to 'C.InterceptableMethod(string)' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. // [InterceptsLocation("Program.cs", 14, 10)] - Diagnostic(ErrorCode.ERR_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(21, 6)); + Diagnostic(ErrorCode.WRN_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(21, 6) + ); } [Fact] @@ -727,4 +728,4 @@ static class D Diagnostic(ErrorCode.ERR_InterceptorSignatureMismatch, "InterceptableMethod").WithArguments("Program.InterceptableMethod(I1, string)", "D.Interceptor1(I1, int)").WithLocation(15, 11) ); } -} \ No newline at end of file +} diff --git a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs index ccc46384a60f2..571cd02976fb6 100644 --- a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs +++ b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs @@ -2937,7 +2937,7 @@ public void TestIsBuildOnlyDiagnostic() case ErrorCode.ERR_EncUpdateFailedDelegateTypeChanged: case ErrorCode.ERR_CannotBeConvertedToUtf8: case ErrorCode.ERR_FileTypeNonUniquePath: - case ErrorCode.ERR_CallNotInterceptable: + case ErrorCode.WRN_CallNotInterceptable: case ErrorCode.ERR_InterceptorSignatureMismatch: Assert.True(isBuildOnly, $"Check failed for ErrorCode.{errorCode}"); break; diff --git a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs index f23cfa65e9abb..100a865044d72 100644 --- a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs +++ b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs @@ -44,7 +44,7 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer "CS8984", // ErrorCode.ERR_EncUpdateFailedDelegateTypeChanged: "CS9026", // ErrorCode.ERR_CannotBeConvertedToUtf8: "CS9068", // ErrorCode.ERR_FileTypeNonUniquePath: - "CS27000", // ErrorCode.ERR_CallNotInterceptable: + "CS27000", // ErrorCode.WRN_CallNotInterceptable: "CS27007" // ErrorCode.ERR_InterceptorSignatureMismatch )] internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics From 774d88c7806f62c553e5f88d5c49565a97221e65 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 22 Mar 2023 12:56:35 -0700 Subject: [PATCH 12/49] note about display locations --- .../Symbols/Source/SourceMethodSymbolWithAttributes.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index fb38f2cabc022..eba911f9d211f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -1032,6 +1032,9 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } + // PROTOTYPE(ic): The attribute should probably be expected to contain "display locations" (1-indexed) a la Diagnostic.ToString(). + // But to do this, we would want to expose helper API for source generators, to produce "display locations" to put in the attribute. + // We would normalize to 0-indexed in this step. all our location-oriented complaints are made here, so we shouldn't need to convert back to "display location" after that point. DeclaringCompilation.AddInterception(new InterceptsLocationAttributeData(filePath, lineNumber, characterNumber, attributeLocation), this); } From 66d3faf77881a6b4a8b6ddeb21e6150d7d028bf3 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 22 Mar 2023 13:16:48 -0700 Subject: [PATCH 13/49] warning boilerplate --- src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs | 1 + src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index d6b7f5b5ecc04..3b06fe5f7c70f 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -530,6 +530,7 @@ internal static int GetWarningLevel(ErrorCode code) case ErrorCode.WRN_ParamsArrayInLambdaOnly: case ErrorCode.WRN_CapturedPrimaryConstructorParameterPassedToBase: case ErrorCode.WRN_UnreadPrimaryConstructorParameter: + case ErrorCode.WRN_CallNotInterceptable: return 1; default: return 0; diff --git a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs index 571cd02976fb6..bb4b38ffd25d5 100644 --- a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs +++ b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs @@ -304,6 +304,7 @@ public void WarningLevel_2() case ErrorCode.WRN_ParamsArrayInLambdaOnly: case ErrorCode.WRN_CapturedPrimaryConstructorParameterPassedToBase: case ErrorCode.WRN_UnreadPrimaryConstructorParameter: + case ErrorCode.WRN_CallNotInterceptable: Assert.Equal(1, ErrorFacts.GetWarningLevel(errorCode)); break; case ErrorCode.WRN_MainIgnored: From d27f9382b87501d3627bbde67b50de092d998e9d Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 22 Mar 2023 16:40:32 -0700 Subject: [PATCH 14/49] Skip failing editor test --- .../Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs index 1b784415bdad9..99b817ce89f10 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs @@ -295,7 +295,7 @@ public enum TestKind PragmasAndSuppressMessageAttributes } - [Theory, CombinatorialData] + [Theory(Skip = "PROTOTYPE(ic): should be able to re-enable after packing error codes"), CombinatorialData] [WorkItem("https://github.com/dotnet/roslyn/issues/46047")] public async Task TestDoNotRemoveUnsupportedDiagnosticSuppression(bool disable, TestKind testKind) { From 2e2d5a0e2164135246400e7faf94f715f253c92d Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 22 Mar 2023 17:35:43 -0700 Subject: [PATCH 15/49] Add feature doc --- docs/features/interceptors.md | 121 ++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/features/interceptors.md diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md new file mode 100644 index 0000000000000..585f59230405d --- /dev/null +++ b/docs/features/interceptors.md @@ -0,0 +1,121 @@ +# Interceptors + +## Summary +[summary]: #summary + +*Interceptors* are an experimental compiler feature. An *interceptor* is a method which can declaratively substitute a call to itself instead of a call to an *interceptable* method at compile time. This substitution occurs by having the interceptor declare the source locations of the calls that it intercepts. This provides a limited facility to change the semantics of existing code by adding new code to a compilation (e.g. in a source generator). + +```cs +using System; +using System.Runtime.CompilerServices; + +var c = new C(); +c.InterceptableMethod(1); // (L1,C1): prints `interceptor 1` +c.InterceptableMethod(1); // (L2,C2): prints `other interceptor 1` +c.InterceptableMethod(1); // prints `interceptable 1` + +class C +{ + [Interceptable] + public void InterceptableMethod(int param) + { + Console.WriteLine($"interceptable {param}"); + } +} + +// generated code +static class D +{ + [InterceptsLocation("Program.cs", line: /*L1*/, character: /*C1*/)] // refers to the call at (L1, C1) + public static void InterceptorMethod(this C c, int param) + { + Console.WriteLine($"interceptor {param}"); + } + + [InterceptsLocation("Program.cs", line: /*L2*/, character: /*C2*/)] // refers to the call at (L2, C2) + public static void OtherInterceptorMethod(this C c, int param) + { + Console.WriteLine($"other interceptor {param}"); + } +} +``` + +## Detailed design +[design]: #detailed-design + +### InterceptableAttribute + +A method must indicate that its calls can be *intercepted* by including `[Interceptable]` on its declaration. + +If a call is intercepted to a method which lacks this attribute, a warning is reported. This may be changed to an error in the future. + +```cs +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Method)] + public sealed class InterceptableAttribute : Attribute { } +} +``` + +### InterceptsLocationAttribute + +A method indicates that it is an *interceptor* by adding one or more `[InterceptsLocation]` attributes. These attributes refer to the source locations of the calls it intercepts. + +```cs +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute + { + } +} +``` + +#### File paths + +File paths used in `[InterceptsLocation]` must exactly match the paths on the syntax trees they refer to by ordinal comparison. `SyntaxTree.FilePath` has already applied `/pathmap` substitution, so the paths used in the attribute will be less environment-specific in many projects. + +The compiler does not map `#line` directives when determining if an `[InterceptsLocation]` attribute intercepts a particular call in syntax. + +#### Position +The implementation currently uses 0-indexed line and character numbers. However, we may want to change that before shipping it as an experimental feature to be 1-indexed, to match existing places where these values are displayed to the user (e.g. `Diagnostic.ToString`). + +The location of the call is the location of the name syntax which denotes the interceptable method. For example, in `app.MapGet(...)`, the name syntax for `MapGet` would be considered the location of the call. If we allow intercepting calls to property accessors in the future (e.g `obj.Property`), we would also be able to use the name syntax in this way. + +#### Attribute creation + +The goal of the above decisions is to make it so that when source generators are filling in `[InterceptsLocation(...)]`, they simply need to read `nameSyntax.SyntaxTree.FilePath` and `nameSyntax.GetLineSpan().Span.Start` for the exact file path and position information they need to use. + +We should provide samples of recommended coding patterns for generator authors, and perhaps provide public helper methods, which will make it relatively easy for them to do the right thing. For example, we could provide a helper on InvocationExpressionSyntax which returns the values that need to be put in `[InterceptsLocation]`. + +### Non-invocation method usages + +Conversion to delegate type, address-of, etc. usages of methods cannot be intercepted. + +Interception can only occur for calls to ordinary member methods--not constructors, delegates, properties, local functions, etc. + +### Arity + +Interceptors cannot have type parameters or be declared in generic types at any level of nesting. + +### Signature matching + +The return and parameter types of the interceptable and interceptor methods must match exactly, except that: +- when an interceptable instance method is compared to a classic extension method, we use the extension method in reduced form for comparison, and +- reference types with oblivious nullability can match either annotated or unannotated reference types. + +Arity does not need to match between intercepted and interceptor methods. In other words, it is permitted to intercept a generic method with a non-generic interceptor. + +### Conflicting interceptors + +If more than one interceptor refers to the same location, it is a compile-time error. + +If an `[InterceptsLocation]` attribute is found in the compilation which does not refer to the location of an interceptable method call, it is a compile-time error. + +### Interceptor accessibility + +An interceptor can intercept a call at a given location even if the interceptor would not ordinarily be accessible at that location. + +### Editor experience + +Interceptors are treated like a post-compilation step in this design. Diagnostics are given for misuse of interceptors, but some diagnostics are only given in the command-line build and not in the IDE. There is limited traceability in the editor for which calls in a compilation are actually being intercepted. If this feature is brought forward past the experimental stage, this limitation will need to be re-examined. From 62c120ef3446367496caff6e9b90e78ab6ac8bfb Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 23 Mar 2023 17:51:24 -0700 Subject: [PATCH 16/49] fix checks for 'this' parameter --- .../CSharp/Portable/CSharpResources.resx | 6 + .../CSharp/Portable/Errors/ErrorCode.cs | 2 + .../CSharp/Portable/Errors/ErrorFacts.cs | 2 + .../LocalRewriter/LocalRewriter_Call.cs | 19 +++ .../Symbols/ReducedExtensionMethodSymbol.cs | 6 + .../SourceMethodSymbolWithAttributes.cs | 2 +- .../Portable/xlf/CSharpResources.cs.xlf | 10 ++ .../Portable/xlf/CSharpResources.de.xlf | 10 ++ .../Portable/xlf/CSharpResources.es.xlf | 10 ++ .../Portable/xlf/CSharpResources.fr.xlf | 10 ++ .../Portable/xlf/CSharpResources.it.xlf | 10 ++ .../Portable/xlf/CSharpResources.ja.xlf | 10 ++ .../Portable/xlf/CSharpResources.ko.xlf | 10 ++ .../Portable/xlf/CSharpResources.pl.xlf | 10 ++ .../Portable/xlf/CSharpResources.pt-BR.xlf | 10 ++ .../Portable/xlf/CSharpResources.ru.xlf | 10 ++ .../Portable/xlf/CSharpResources.tr.xlf | 10 ++ .../Portable/xlf/CSharpResources.zh-Hans.xlf | 10 ++ .../Portable/xlf/CSharpResources.zh-Hant.xlf | 10 ++ .../Semantic/Semantics/InterceptorsTests.cs | 151 +++++++++++++++--- .../Test/Syntax/Diagnostics/DiagnosticTest.cs | 2 + .../CSharpLspBuildOnlyDiagnostics.cs | 4 +- 22 files changed, 300 insertions(+), 24 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 055deb03196f9..4d041df186411 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7541,4 +7541,10 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 99a3b6ab8209a..b9f21aa6deae7 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2202,6 +2202,8 @@ internal enum ErrorCode ERR_InterceptableMethodMustBeOrdinary = 27008, ERR_InterceptorMethodMustBeOrdinary = 27009, ERR_InterceptorMustReferToStartOfTokenPosition = 27010, + ERR_InterceptorMustHaveMatchingThisParameter = 27011, + ERR_InterceptorMustNotHaveThisParameter = 27012, #endregion // Note: you will need to do the following after adding warnings: diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 3b06fe5f7c70f..8dc9015d92277 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -582,6 +582,8 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_FileTypeNonUniquePath: case ErrorCode.WRN_CallNotInterceptable: case ErrorCode.ERR_InterceptorSignatureMismatch: + case ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter: + case ErrorCode.ERR_InterceptorMustNotHaveThisParameter: // 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_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 12730ce146c6f..7c601f8fa7189 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -179,6 +179,25 @@ bool invokedAsExtensionMethod return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); } + // PROTOTYPE(ic): should we also reduce 'method' when comparing, if it is being called as an extension method? + + method.TryGetThisParameter(out var methodThisParameter); + symbolForCompare.TryGetThisParameter(out var interceptorThisParameterForCompare); + switch (methodThisParameter, interceptorThisParameterForCompare) + { + case (not null, null): + case (not null, not null) when !methodThisParameter.Type.Equals(interceptorThisParameterForCompare.Type, TypeCompareKind.ObliviousNullableModifierMatchesAny) + || methodThisParameter.RefKind != interceptorThisParameterForCompare.RefKind: // PROTOTYPE(ic): and ref custom modifiers are equal? + this._diagnostics.Add(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, interceptsLocationAttributeData.AttributeLocation, methodThisParameter, method); + break; + case (null, not null): + this._diagnostics.Add(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, interceptsLocationAttributeData.AttributeLocation, method); + break; + default: + break; + + } + if (needToReduce) { Debug.Assert(!method.IsStatic); diff --git a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs index ea3dc1077c72c..76cda79d142d3 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs @@ -530,6 +530,12 @@ internal override bool IsExplicitInterfaceImplementation internal override bool IsEffectivelyReadOnly => _reducedFrom.Parameters[0].RefKind == RefKind.In; + internal override bool TryGetThisParameter(out ParameterSymbol thisParameter) + { + thisParameter = _reducedFrom.Parameters[0]; + return true; + } + public override ImmutableArray ExplicitInterfaceImplementations { get { return ImmutableArray.Empty; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index eba911f9d211f..b2e97954aef8a 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -1004,7 +1004,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments var lineLength = line.End - line.Start; if (characterNumber >= lineLength) { - diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeLocation, referencedLines.Count, characterNumber); + diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeLocation, lineLength, characterNumber); return; } diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 40b83a85a2d22..caa8117651284 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 553fb710432ae..5c8c7ea02da26 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 439a57843949e..55a9633d7c1cb 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 96631155ffa23..4fbf5301538b4 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index d5308fdcf8672..f3e89b8543023 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index 90443011ef819..bf1be701d7722 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index 94ba498fea3e7..f2a2a31bdbbce 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index 842993f12691b..dc38a916fda95 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 7b8fc100725b4..2e3ee8abc6d6d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index c8c009fe98d05..c3e361ec484e9 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 05ea47434742b..8310dda033b0d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 34318bb26edf3..8cd6aee302d43 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index 231d15f8d64a1..90ad1b588bf72 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -862,6 +862,16 @@ An interceptor method must be an ordinary member method. + + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + Interceptor must have a 'this' parameter matching parameter '{0}' on '{1}'. + + + + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 6edc6d3fc4678..b4f3618855f43 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -127,11 +127,10 @@ public void InterceptableInstanceMethod_InterceptorExtensionMethod() using System.Runtime.CompilerServices; using System; - interface I1 { } - class C : I1 + class C { [Interceptable] - public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + public C InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } } static class Program @@ -145,14 +144,46 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 15, 10)] - public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + [InterceptsLocation("Program.cs", 14, 10)] + public static C Interceptor1(this C 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 ArgumentLabels() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public void InterceptableMethod(string s1, string s2) { Console.Write(s1 + s2); } + } + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod(s2: "World", s1: "Hello "); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + public static void Interceptor1(this C c, string s1, string s2) { Console.Write("interceptor " + s1 + s2); } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor Hello World"); + verifier.VerifyDiagnostics(); + } + [Fact] public void InterceptableExtensionMethod_InterceptorExtensionMethod_Sequence() { @@ -193,11 +224,10 @@ public void InterceptableFromMetadata() using System.Runtime.CompilerServices; using System; - public interface I1 { } - public class C : I1 + public class C { [Interceptable] - public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + public C InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } } """; @@ -217,7 +247,7 @@ public static void Main() static class D { [InterceptsLocation("Program.cs", 8, 10)] - public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } } """; @@ -305,10 +335,9 @@ public void CallNotInterceptable() using System.Runtime.CompilerServices; using System; - interface I1 { } - class C : I1 + class C { - public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + public C InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } } static class Program @@ -322,15 +351,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] - public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + [InterceptsLocation("Program.cs", 13, 10)] + public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } } """; - var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); - comp.VerifyEmitDiagnostics( - // Program.cs(21,6): warning CS27000: Cannot intercept a call to 'C.InterceptableMethod(string)' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - // [InterceptsLocation("Program.cs", 14, 10)] - Diagnostic(ErrorCode.WRN_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(21, 6) + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); + verifier.VerifyDiagnostics( + // Program.cs(20,6): warning CS27000: Cannot intercept a call to 'C.InterceptableMethod(string)' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + // [InterceptsLocation("Program.cs", 13, 10)] + Diagnostic(ErrorCode.WRN_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 13, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(20, 6) ); } @@ -615,9 +644,9 @@ static class D """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(21,6): error CS27006: The given line is '23' characters long, which is fewer than the provided character number '1000'. - // [InterceptsLocation("Program.cs", 15, 1000)] - Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 15, 1000)").WithArguments("23", "1000").WithLocation(21, 6) + // Program.cs(21,6): error CS27006: The given line is '5' characters long, which is fewer than the provided character number '1000'. + // [InterceptsLocation("Program.cs", 15, 1000)] + Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 15, 1000)").WithArguments("5", "1000").WithLocation(21, 6) ); } @@ -728,4 +757,82 @@ static class D Diagnostic(ErrorCode.ERR_InterceptorSignatureMismatch, "InterceptableMethod").WithArguments("Program.InterceptableMethod(I1, string)", "D.Interceptor1(I1, int)").WithLocation(15, 11) ); } + + [Fact] + public void SignatureMismatch_02() + { + // Instance method receiver type differs from interceptor 'this' parameter type. + var source = """ + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 + { + [Interceptable] + public I1 InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 10)] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(22,6): error CS27011: Interceptor must have a 'this' parameter matching parameter 'C this' on 'C.InterceptableMethod(string)'. + // [InterceptsLocation("Program.cs", 15, 10)] + Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 15, 10)").WithArguments("C this", "C.InterceptableMethod(string)").WithLocation(22, 6) + ); + } + + // PROTOTYPE(ic): test interceptable explicit interface implementation (should error). + // PROTOTYPE(ic): test interceptor with 'ref this' to match a struct interceptable method. + + [Fact] + public void SignatureMismatch_03() + { + // Instance method 'this' parameter ref kind differs from interceptor 'this' parameter ref kind. + var source = """ + using System.Runtime.CompilerServices; + using System; + + struct S + { + [Interceptable] + public void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + } + + static class Program + { + public static void Main() + { + var s = new S(); + s.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + public static void Interceptor1(this S s, string param) { Console.Write("interceptor " + param); } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27011: Interceptor must have a 'this' parameter matching parameter 'ref S this' on 'S.InterceptableMethod(string)'. + // [InterceptsLocation("Program.cs", 14, 10)] + Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("ref S this", "S.InterceptableMethod(string)").WithLocation(21, 6) + ); + } } diff --git a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs index bb4b38ffd25d5..dba8f95929a5b 100644 --- a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs +++ b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs @@ -2940,6 +2940,8 @@ public void TestIsBuildOnlyDiagnostic() case ErrorCode.ERR_FileTypeNonUniquePath: case ErrorCode.WRN_CallNotInterceptable: case ErrorCode.ERR_InterceptorSignatureMismatch: + case ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter: + case ErrorCode.ERR_InterceptorMustNotHaveThisParameter: Assert.True(isBuildOnly, $"Check failed for ErrorCode.{errorCode}"); break; diff --git a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs index 100a865044d72..30dcbe4de81ea 100644 --- a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs +++ b/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs @@ -45,7 +45,9 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer "CS9026", // ErrorCode.ERR_CannotBeConvertedToUtf8: "CS9068", // ErrorCode.ERR_FileTypeNonUniquePath: "CS27000", // ErrorCode.WRN_CallNotInterceptable: - "CS27007" // ErrorCode.ERR_InterceptorSignatureMismatch + "CS27007", // ErrorCode.ERR_InterceptorSignatureMismatch + "CS27011", // ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter + "CS27012" // ErrorCode.ERR_InterceptorMustNotHaveThisParameter )] internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics { From 7601766b48e1ac09e44ca846d84a16d8704fd03e Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 23 Mar 2023 17:52:29 -0700 Subject: [PATCH 17/49] add test reminder --- .../CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 7c601f8fa7189..bfaf3d611859f 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -191,6 +191,7 @@ bool invokedAsExtensionMethod this._diagnostics.Add(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, interceptsLocationAttributeData.AttributeLocation, methodThisParameter, method); break; case (null, not null): + // PROTOTYPE(ic): test intercepting a static method with an instance method this._diagnostics.Add(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, interceptsLocationAttributeData.AttributeLocation, method); break; default: From 10b9a41aaab4931378c4f4c765bbef1307ba6298 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 23 Mar 2023 18:12:34 -0700 Subject: [PATCH 18/49] more --- .../LocalRewriter/LocalRewriter_Call.cs | 1 - .../Semantic/Semantics/InterceptorsTests.cs | 33 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index bfaf3d611859f..7c601f8fa7189 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -191,7 +191,6 @@ bool invokedAsExtensionMethod this._diagnostics.Add(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, interceptsLocationAttributeData.AttributeLocation, methodThisParameter, method); break; case (null, not null): - // PROTOTYPE(ic): test intercepting a static method with an instance method this._diagnostics.Add(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, interceptsLocationAttributeData.AttributeLocation, method); break; default: diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index b4f3618855f43..8ed554956db47 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -152,6 +152,39 @@ static class D verifier.VerifyDiagnostics(); } + [Fact] + public void InterceptableStaticMethod_InterceptorInstanceMethod() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + static class Program + { + public static void Main() + { + C.InterceptableMethod("call site"); + } + } + + class C + { + [Interceptable] + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + + [InterceptsLocation("Program.cs", 7, 10)] + public void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(17,6): error CS27012: Interceptor must not have a 'this' parameter because 'C.InterceptableMethod(string)' does not have a 'this' parameter. + // [InterceptsLocation("Program.cs", 7, 10)] + Diagnostic(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, @"InterceptsLocation(""Program.cs"", 7, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(17, 6)); + } + + // 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. + [Fact] public void ArgumentLabels() { From 8b694c1bf2e94274b0e68d5a6e1cb77dddf119e7 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Fri, 24 Mar 2023 14:15:32 -0700 Subject: [PATCH 19/49] Address a little feedback --- docs/features/interceptors.md | 24 ++++--- .../Semantic/Semantics/InterceptorsTests.cs | 65 ++++++++++++++++++- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 585f59230405d..d49a54ee44517 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -3,16 +3,16 @@ ## Summary [summary]: #summary -*Interceptors* are an experimental compiler feature. An *interceptor* is a method which can declaratively substitute a call to itself instead of a call to an *interceptable* method at compile time. This substitution occurs by having the interceptor declare the source locations of the calls that it intercepts. This provides a limited facility to change the semantics of existing code by adding new code to a compilation (e.g. in a source generator). +*Interceptors* are an experimental compiler feature. An *interceptor* is a method which can declaratively substitute a call to an *interceptable* method with a call to itself at compile time. This substitution occurs by having the interceptor declare the source locations of the calls that it intercepts. This provides a limited facility to change the semantics of existing code by adding new code to a compilation (e.g. in a source generator). ```cs using System; using System.Runtime.CompilerServices; var c = new C(); -c.InterceptableMethod(1); // (L1,C1): prints `interceptor 1` -c.InterceptableMethod(1); // (L2,C2): prints `other interceptor 1` -c.InterceptableMethod(1); // prints `interceptable 1` +c.InterceptableMethod(1); // (L1,C1): prints "interceptor 1" +c.InterceptableMethod(1); // (L2,C2): prints "other interceptor 1" +c.InterceptableMethod(1); // prints "interceptable 1" class C { @@ -47,7 +47,7 @@ static class D A method must indicate that its calls can be *intercepted* by including `[Interceptable]` on its declaration. -If a call is intercepted to a method which lacks this attribute, a warning is reported. This may be changed to an error in the future. +If a call is intercepted to a method which lacks this attribute, a warning is reported, and interception still occurs. This may be changed to an error in the future. ```cs namespace System.Runtime.CompilerServices @@ -80,7 +80,7 @@ The compiler does not map `#line` directives when determining if an `[Intercepts #### Position The implementation currently uses 0-indexed line and character numbers. However, we may want to change that before shipping it as an experimental feature to be 1-indexed, to match existing places where these values are displayed to the user (e.g. `Diagnostic.ToString`). -The location of the call is the location of the name syntax which denotes the interceptable method. For example, in `app.MapGet(...)`, the name syntax for `MapGet` would be considered the location of the call. If we allow intercepting calls to property accessors in the future (e.g `obj.Property`), we would also be able to use the name syntax in this way. +The location of the call is the location of the name syntax which denotes the interceptable method. For example, in `app.MapGet(...)`, the name syntax for `MapGet` would be considered the location of the call. For a static method call like `System.Console.WriteLine(...)`, the name syntax for `WriteLine` is the location of the call. If we allow intercepting calls to property accessors in the future (e.g `obj.Property`), we would also be able to use the name syntax in this way. #### Attribute creation @@ -100,9 +100,13 @@ Interceptors cannot have type parameters or be declared in generic types at any ### Signature matching -The return and parameter types of the interceptable and interceptor methods must match exactly, except that: -- when an interceptable instance method is compared to a classic extension method, we use the extension method in reduced form for comparison, and -- reference types with oblivious nullability can match either annotated or unannotated reference types. +When a call is intercepted, the interceptor and interceptable methods must meet the signature matching requirements detailed below: +- When an interceptable instance method is compared to a classic extension method, we use the extension method in reduced form for comparison. The extension method parameter with the 'this' modifier is compared to the instance method 'this' parameter. +- The returns and parameters, including the 'this' parameter, must have the same ref kinds and types, except that reference types with oblivious nullability can match either annotated or unannotated reference types. +- Method names and parameter names are not required to match. +- Parameter default values are not required to match. +- 'scoped' modifiers and `[UnscopedRefAttribute]` must be equivalent. + Arity does not need to match between intercepted and interceptor methods. In other words, it is permitted to intercept a generic method with a non-generic interceptor. @@ -110,7 +114,7 @@ Arity does not need to match between intercepted and interceptor methods. In oth If more than one interceptor refers to the same location, it is a compile-time error. -If an `[InterceptsLocation]` attribute is found in the compilation which does not refer to the location of an interceptable method call, it is a compile-time error. +If an `[InterceptsLocation]` attribute is found in the compilation which does not refer to the location of an explicit method call, it is a compile-time error. ### Interceptor accessibility diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 8ed554956db47..d0b61ae3c7f2c 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; public class InterceptorsTests : CSharpTestBase { - private static (string, string) s_attributesSource = (""" + private readonly static (string, string) s_attributesSource = (""" namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method)] @@ -84,6 +84,69 @@ class D verifier.VerifyDiagnostics(); } + [Fact(Skip = "PROTOTYPE(ic): produce an error here")] + public void Accessibility_01() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod() { Console.Write("interceptable"); } + + public static void Main() + { + InterceptableMethod(); + } + } + + class D + { + [InterceptsLocation("Program.cs", 10, 8)] + private static void Interceptor1() { Console.Write("interceptor 1"); } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor 1", verify: Verification.Fails); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void Accessibility_02() + { + var source1 = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod() { Console.Write("interceptable"); } + + public static void Main() + { + InterceptableMethod(); + } + } + """; + + var source2 = """ + using System.Runtime.CompilerServices; + using System; + + file class D + { + [InterceptsLocation("Program.cs", 10, 8)] + public static void Interceptor1() { Console.Write("interceptor 1"); } + } + """; + + // PROTOTYPE(ic): Consider if an error should be reported here. It's not possible to directly use 'Interceptor1' at the location of the intercepted call. + var verifier = CompileAndVerify(new[] { (source1, "Program.cs"), (source2, "Other.cs"), s_attributesSource }, expectedOutput: "interceptor 1"); + verifier.VerifyDiagnostics(); + } + [Fact] public void InterceptableExtensionMethod_InterceptorExtensionMethod() { From 189338e21775d91a4f339743611d875ed2161f0c Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 27 Mar 2023 16:19:57 -0700 Subject: [PATCH 20/49] Address more feedback --- docs/features/interceptors.md | 2 +- .../Portable/BoundTree/BoundExpression.cs | 19 +- .../CSharp/Portable/CSharpResources.resx | 2 +- .../LocalRewriter/LocalRewriter_Call.cs | 91 ++++----- .../SourceMethodSymbolWithAttributes.cs | 6 +- .../Portable/xlf/CSharpResources.cs.xlf | 4 +- .../Portable/xlf/CSharpResources.de.xlf | 4 +- .../Portable/xlf/CSharpResources.es.xlf | 4 +- .../Portable/xlf/CSharpResources.fr.xlf | 4 +- .../Portable/xlf/CSharpResources.it.xlf | 4 +- .../Portable/xlf/CSharpResources.ja.xlf | 4 +- .../Portable/xlf/CSharpResources.ko.xlf | 4 +- .../Portable/xlf/CSharpResources.pl.xlf | 4 +- .../Portable/xlf/CSharpResources.pt-BR.xlf | 4 +- .../Portable/xlf/CSharpResources.ru.xlf | 4 +- .../Portable/xlf/CSharpResources.tr.xlf | 4 +- .../Portable/xlf/CSharpResources.zh-Hans.xlf | 4 +- .../Portable/xlf/CSharpResources.zh-Hant.xlf | 4 +- .../Semantic/Semantics/InterceptorsTests.cs | 181 +++++++++++++++++- .../Attributes/AttributeDescription.cs | 3 +- 20 files changed, 260 insertions(+), 96 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index d49a54ee44517..1512acbb915e1 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -118,7 +118,7 @@ If an `[InterceptsLocation]` attribute is found in the compilation which does no ### Interceptor accessibility -An interceptor can intercept a call at a given location even if the interceptor would not ordinarily be accessible at that location. +An interceptor must be accessible at the location where interception is occurring. ### Editor experience diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index f1e27b4b92b85..64f0e1c48553f 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -244,19 +244,22 @@ public Location? InterceptableLocation { get { - if (this.Syntax is not InvocationExpressionSyntax syntax) + if (this.WasCompilerGenerated || this.Syntax is not InvocationExpressionSyntax syntax) { return null; } - if (syntax.Expression is MemberAccessExpressionSyntax memberAccess) - { - return memberAccess.Name.Location; - } - else + // If a qualified name is used as a valid receiver of an invocation syntax at some point, + // we probably want to treat it similarly to a MemberAccessExpression. + // However, we don't expect to encounter it. + Debug.Assert(syntax.Expression is not QualifiedNameSyntax); + + return syntax.Expression switch { - return syntax.Expression.Location; - } + MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Location, + SimpleNameSyntax name => name.Location, + _ => null + }; } } } diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 4d041df186411..11f8c36c04773 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7527,7 +7527,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 7c601f8fa7189..cc09e663884ad 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -133,27 +133,18 @@ private void EmbedIfNeedTo(BoundExpression receiver, ImmutableArray arguments, - ImmutableArray argsToParamsOpt, - ImmutableArray argumentRefKindsOpt, - bool invokedAsExtensionMethod - ) InterceptCallAndAdjustArguments(BoundCall node) + public void InterceptCallAndAdjustArguments( + ref MethodSymbol method, + ref BoundExpression? receiverOpt, + ref ImmutableArray arguments, + ref ImmutableArray argumentRefKindsOpt, + ref bool invokedAsExtensionMethod, + Location? interceptableLocation) { - var method = node.Method; - var receiverOpt = node.ReceiverOpt; - var arguments = node.Arguments; - var argsToParamsOpt = node.ArgsToParamsOpt; - var argumentRefKindsOpt = node.ArgumentRefKindsOpt; - var invokedAsExtensionMethod = node.InvokedAsExtensionMethod; - - var interceptableLocation = node.InterceptableLocation; if (this._compilation.GetInterceptor(interceptableLocation) is not var (interceptsLocationAttributeData, interceptor)) { // The call was not intercepted. - return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + return; } Debug.Assert(interceptableLocation != null); @@ -171,15 +162,16 @@ bool invokedAsExtensionMethod // When the original call is to an instance method, and the interceptor is an extension method, // we need to take special care to intercept with the extension method as though it is being called in reduced form. - var needToReduce = receiverOpt != null && interceptor.IsExtensionMethod; + Debug.Assert(receiverOpt is not BoundTypeExpression || method.IsStatic); + var needToReduce = receiverOpt is not (null or BoundTypeExpression) && interceptor.IsExtensionMethod; var symbolForCompare = needToReduce ? ReducedExtensionMethodSymbol.Create(interceptor, receiverOpt!.Type, _compilation) : interceptor; - if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare)) + if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare)) // PROTOTYPE(ic): also checked for 'scoped'/'[UnscopedRef]' differences { this._diagnostics.Add(ErrorCode.ERR_InterceptorSignatureMismatch, interceptableLocation, method, interceptor); - return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + return; } - // PROTOTYPE(ic): should we also reduce 'method' when comparing, if it is being called as an extension method? + // PROTOTYPE(ic): should we also reduce both methods if 'method' is being called as an extension method? method.TryGetThisParameter(out var methodThisParameter); symbolForCompare.TryGetThisParameter(out var interceptorThisParameterForCompare); @@ -200,46 +192,38 @@ bool invokedAsExtensionMethod if (needToReduce) { - Debug.Assert(!method.IsStatic); + Debug.Assert(methodThisParameter is not null); arguments = arguments.Insert(0, receiverOpt!); receiverOpt = null; - var thisParameter = method.ThisParameter; - - if (!argsToParamsOpt.IsDefault) + // CodeGenerator.EmitArguments requires that we have a fully-filled-out argumentRefKindsOpt for any ref/in/out arguments. + var thisRefKind = methodThisParameter.RefKind; + if (argumentRefKindsOpt.IsDefault && thisRefKind != RefKind.None) { - var argsToParamsBuilder = ImmutableArray.CreateBuilder(argsToParamsOpt.Length + 1); - argsToParamsBuilder.Add(0); // argument 0 always corresponds to parameter 0 (extension this) - foreach (var paramOrdinal in argsToParamsOpt) - { - // the param that each argument *really* refers to is pushed forward by 1. - // e.g. we're adapting: - // void Type.Method(int param) { } - // void Ext.Method(this Type type, int param) { } - argsToParamsBuilder.Add(paramOrdinal + 1); - } - - argsToParamsOpt = argsToParamsBuilder.MoveToImmutable(); + argumentRefKindsOpt = method.Parameters.SelectAsArray(static param => param.RefKind); } if (!argumentRefKindsOpt.IsDefault) { - argumentRefKindsOpt = argumentRefKindsOpt.Insert(0, thisParameter.RefKind); + argumentRefKindsOpt = argumentRefKindsOpt.Insert(0, thisRefKind); } + // PROTOTYPE(ic): search for a scenario where propagating this value matters invokedAsExtensionMethod = true; } method = interceptor; - return (method, receiverOpt, arguments, argsToParamsOpt, argumentRefKindsOpt, invokedAsExtensionMethod); + return; } public override BoundNode VisitCall(BoundCall node) { Debug.Assert(node != null); - var (method, receiverOpt, arguments, argsToParamsOpt, argRefKindsOpt, invokedAsExtensionMethod) = InterceptCallAndAdjustArguments(node); + // PROTOTYPE(ic): interception should occur after lowering + // so that we use the original method in all relevant situations such as interpolated string lowering. + var (method, receiverOpt, arguments, argsToParamsOpt, argRefKindsOpt, invokedAsExtensionMethod) = (node.Method, node.ReceiverOpt, node.Arguments, node.ArgsToParamsOpt, node.ArgumentRefKindsOpt, node.InvokedAsExtensionMethod); // Rewrite the receiver BoundExpression? rewrittenReceiver = VisitExpression(receiverOpt); @@ -255,19 +239,20 @@ public override BoundNode VisitCall(BoundCall node) storesOpt: null, ref temps); - var rewrittenCall = MakeArgumentsAndCall( - syntax: node.Syntax, - rewrittenReceiver: rewrittenReceiver, - method: method, - arguments: rewrittenArguments, - argumentRefKindsOpt: argRefKindsOpt, - expanded: node.Expanded, // PROTOTYPE(ic): params differences shouldn't matter--maybe even make it an error--but we need to test - invokedAsExtensionMethod: invokedAsExtensionMethod, - argsToParamsOpt: argsToParamsOpt, - resultKind: node.ResultKind, - type: node.Type, - temps, - nodeOpt: node); + rewrittenArguments = MakeArguments( + node.Syntax, + rewrittenArguments, + method, + node.Expanded, // PROTOTYPE(ic): params differences shouldn't matter--maybe even make it an error--but we need to test + argsToParamsOpt, + ref argRefKindsOpt, + ref temps, + invokedAsExtensionMethod); + + InterceptCallAndAdjustArguments(ref method, ref rewrittenReceiver, ref rewrittenArguments, ref argRefKindsOpt, ref invokedAsExtensionMethod, node.InterceptableLocation); + + // PROTOTYPE(ic): intercept with object.ReferenceEquals? + var rewrittenCall = MakeCall(node, node.Syntax, rewrittenReceiver, method, rewrittenArguments, argRefKindsOpt, invokedAsExtensionMethod, node.ResultKind, node.Type, temps.ToImmutableAndFree()); if (Instrument) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index b2e97954aef8a..370a0f6ad80cb 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -13,7 +13,6 @@ using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Symbols @@ -959,7 +958,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; var attributeLocation = arguments.AttributeSyntaxOpt.Location; - if (Arity != 0 || ContainingType.VisitType(static (type, _, _) => type.GetArity() != 0, arg: (object?)null) is not null) + if (Arity != 0 || ContainingType.IsGenericType) { // PROTOTYPE(ic): for now, let's disallow type arguments on the method or containing types. // eventually, we could consider doing a type argument inference, seeing if the interceptor's type constraints are met, etc... @@ -1022,9 +1021,8 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } - // Did they actually refer to the start of the token, not the middle? + // Did they actually refer to the start of the token, not the middle, or in leading trivia? var tokenPositionDifference = referencedPosition - referencedToken.Span.Start; - Debug.Assert(tokenPositionDifference >= 0); // if the referenced position were smaller than the token's start position, we wouldn't have matched it. if (tokenPositionDifference != 0) { // tokens don't span multiple lines, so we can apply the difference we found to the characterNumber within the line and figure out which character the user should have used. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index caa8117651284..629b77cd67b27 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 5c8c7ea02da26..1ab4f238db080 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 55a9633d7c1cb..d0b36e6614a50 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 4fbf5301538b4..c2fc03c44660f 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index f3e89b8543023..118b3d28b8ef3 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index bf1be701d7722..ef1b31e0a01c7 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index f2a2a31bdbbce..53fab1103a6c0 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index dc38a916fda95..b7e205b4d0958 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 2e3ee8abc6d6d..e9974d3de47be 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index c3e361ec484e9..a825cb95ad61d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 8310dda033b0d..e45dfa554297d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 8cd6aee302d43..ab1ea558bef6f 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index 90ad1b588bf72..ce6c2f2f7e4a1 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -888,8 +888,8 @@ - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. - The provided line and character number does not refer to an interceptable method, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index d0b61ae3c7f2c..9100e122eaf8a 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -18,7 +18,7 @@ namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method)] public sealed class InterceptableAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public sealed class InterceptsLocationAttribute : Attribute { public InterceptsLocationAttribute(string filePath, int line, int column) @@ -215,6 +215,38 @@ static class D verifier.VerifyDiagnostics(); } + [Fact] + public void InterceptableStaticMethod_InterceptorExtensionMethod() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static C InterceptableMethod(C c, string param) { Console.Write("interceptable " + param); return c; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + C.InterceptableMethod(c, "call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 10)] + public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); + verifier.VerifyDiagnostics(); + } + [Fact] public void InterceptableStaticMethod_InterceptorInstanceMethod() { @@ -818,6 +850,49 @@ static class D ); } + [Fact] + public void InterceptsLocationBadPosition_05() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C { } + + static class Program + { + public static void Main() + { + var c = new C(); + c. + InterceptableMethod("call site"); + + c.InterceptableMethod ("call site"); + } + + [Interceptable] + public static C InterceptableMethod(this C c, string param) { Console.Write("interceptable " + param); return c; } + + [InterceptsLocation("Program.cs", 11, 10)] // intercept spaces before 'InterceptableMethod' token + [InterceptsLocation("Program.cs", 13, 32)] // intercept spaces after 'InterceptableMethod' token + public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } + } + + static class CExt + { + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(20,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '12' instead. + // [InterceptsLocation("Program.cs", 11, 10)] // intercept spaces before 'InterceptableMethod' token + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 11, 10)").WithArguments("InterceptableMethod", "12").WithLocation(20, 6), + // Program.cs(21,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '10' instead. + // [InterceptsLocation("Program.cs", 13, 32)] // intercept spaces after 'InterceptableMethod' token + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 13, 32)").WithArguments("InterceptableMethod", "10").WithLocation(21, 6) + ); + } + [Fact] public void SignatureMismatch_01() { @@ -931,4 +1006,108 @@ static class D Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("ref S this", "S.InterceptableMethod(string)").WithLocation(21, 6) ); } + + [Fact] + public void InterpolatedStringHandler_01() + { + // Verify that interpolated string-related attributes on an intercepted call use the attributes from the interceptable method. + var code = """ +using System; +using System.Runtime.CompilerServices; + +var s = new S1(); +s.M($""); + +public struct S1 +{ + public S1() { } + public int Field = 1; + + [Interceptable] + public void M([InterpolatedStringHandlerArgument("")] CustomHandler c) + { + Console.Write(0); + } +} + +public static class S1Ext +{ + [InterceptsLocation("Program.cs", 4, 2)] + public static void M1(ref this S1 s1, CustomHandler c) + { + Console.Write(2); + } +} + +partial struct CustomHandler +{ + public CustomHandler(int literalLength, int formattedCount, S1 s) + { + Console.Write(1); + } +} +"""; + var verifier = CompileAndVerify( + new[] + { + (code, "Program.cs"), + (InterpolatedStringHandlerArgumentAttribute, "a.cs"), + (GetInterpolatedStringCustomHandlerType("CustomHandler", "partial struct", useBoolReturns: false), "b.cs"), + s_attributesSource + }, + expectedOutput: "12"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InterpolatedStringHandler_02() + { + // Verify that interpolated string-related attributes are ignored on an interceptor in an intercepted call. + var code = """ +using System; +using System.Runtime.CompilerServices; + +var s = new S1(); +s.M($""); + +public struct S1 +{ + public S1() { } + public int Field = 1; + + [Interceptable] + public void M(CustomHandler c) + { + Console.Write(0); + } +} + +public static class S1Ext +{ + [InterceptsLocation("Program.cs", 4, 2)] + public static void M1(ref this S1 s1, [InterpolatedStringHandlerArgument("s1")] CustomHandler c) + { + Console.Write(1); + } +} + +partial struct CustomHandler +{ + public CustomHandler(int literalLength, int formattedCount, S1 s) + { + throw null!; // we don't expect this to be called + } +} +"""; + var verifier = CompileAndVerify( + new[] + { + (code, "Program.cs"), + (InterpolatedStringHandlerArgumentAttribute, "a.cs"), + (GetInterpolatedStringCustomHandlerType("CustomHandler", "partial struct", useBoolReturns: false), "b.cs"), + s_attributesSource + }, + expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } } diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs index dca8f2c12a8e9..b154a4e01d530 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs @@ -224,7 +224,6 @@ static AttributeDescription() private static readonly byte[][] s_signaturesOfMemberNotNullAttribute = { s_signature_HasThis_Void_String, s_signature_HasThis_Void_SzArray_String }; private static readonly byte[][] s_signaturesOfMemberNotNullWhenAttribute = { s_signature_HasThis_Void_Boolean_String, s_signature_HasThis_Void_Boolean_SzArray_String }; private static readonly byte[][] s_signaturesOfFixedBufferAttribute = { s_signature_HasThis_Void_Type_Int32 }; - private static readonly byte[][] s_signaturesOfInterceptableAttribute = { s_signature_HasThis_Void }; private static readonly byte[][] s_signaturesOfInterceptsLocationAttribute = { s_signature_HasThis_Void_String_Int32_Int32 }; private static readonly byte[][] s_signaturesOfPrincipalPermissionAttribute = { s_signature_HasThis_Void_SecurityAction }; private static readonly byte[][] s_signaturesOfPermissionSetAttribute = { s_signature_HasThis_Void_SecurityAction }; @@ -388,7 +387,7 @@ static AttributeDescription() internal static readonly AttributeDescription StructLayoutAttribute = new AttributeDescription("System.Runtime.InteropServices", "StructLayoutAttribute", s_signaturesOfStructLayoutAttribute); internal static readonly AttributeDescription FieldOffsetAttribute = new AttributeDescription("System.Runtime.InteropServices", "FieldOffsetAttribute", s_signaturesOfFieldOffsetAttribute); internal static readonly AttributeDescription FixedBufferAttribute = new AttributeDescription("System.Runtime.CompilerServices", "FixedBufferAttribute", s_signaturesOfFixedBufferAttribute); - internal static readonly AttributeDescription InterceptableAttribute = new AttributeDescription("System.Runtime.CompilerServices", "InterceptableAttribute", s_signaturesOfInterceptableAttribute); + internal static readonly AttributeDescription InterceptableAttribute = new AttributeDescription("System.Runtime.CompilerServices", "InterceptableAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription InterceptsLocationAttribute = new AttributeDescription("System.Runtime.CompilerServices", "InterceptsLocationAttribute", s_signaturesOfInterceptsLocationAttribute); internal static readonly AttributeDescription AllowNullAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "AllowNullAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription DisallowNullAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "DisallowNullAttribute", s_signatures_HasThis_Void_Only); From 708b53aeff8219e461c146cf55f27efa8a6eef4c Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 27 Mar 2023 16:34:41 -0700 Subject: [PATCH 21/49] fix style error --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 9100e122eaf8a..e75b84987289e 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; public class InterceptorsTests : CSharpTestBase { - private readonly static (string, string) s_attributesSource = (""" + private static readonly (string, string) s_attributesSource = (""" namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method)] From 49a459e8805e94541d6a7c8ae4971c7d97182ad9 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 27 Mar 2023 16:41:44 -0700 Subject: [PATCH 22/49] Adjust test for ReducedExtensionMethodSymbol.ThisParameter --- .../CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs index 2b5e6d04c03bc..e25babdc31e6d 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs @@ -2745,8 +2745,9 @@ static void Main(string[] args) methodSymbol = (MethodSymbol)symbolInfo.Symbol.GetSymbol(); Assert.False(methodSymbol.IsFromCompilation(compilation)); - // 9341 is resolved as Won't Fix since ThisParameter property is internal. - Assert.Throws(() => methodSymbol.ThisParameter); + parameter = methodSymbol.ThisParameter; + Assert.Equal(-1, parameter.Ordinal); + Assert.Equal(parameter.ContainingSymbol, methodSymbol); } private CompilationVerifier CompileAndVerify(string source, string expectedOutput = null, Action validator = null, From 38b650021a8bbf51f8a698d138d1cb80a980f859 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 28 Mar 2023 17:20:18 -0700 Subject: [PATCH 23/49] Use a long for PEMethodSymbol.PackedFlags --- .../Symbols/Metadata/PE/PEMethodSymbol.cs | 98 ++++++++++--------- .../Semantic/Semantics/InterceptorsTests.cs | 2 + .../InternalUtilities/EnumUtilties.cs | 4 +- .../ThreadSafeFlagOperations.cs | 16 +++ 4 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs index 8c495af1a51d2..26eac336d3961 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs @@ -45,10 +45,12 @@ public SignatureData(SignatureHeader header, ImmutableArray par // This type is used to compact many different bits of information efficiently. private struct PackedFlags { - // We currently pack everything into a 32-bit int with the following layout: + // We currently pack everything into a 64-bit long with the following layout: // - // |y|x|w|v|u|t|s|r|q|p|ooo|n|m|l|k|j|i|h|g|f|e|d|c|b|aaaaa| + // |________|________|________|_______|A|z|y|x|w|v|u|t|s|r|q|p|ooo|n|m|l|k|j|i|h|g|f|e|d|c|b|aaaaa| // + // _ = unused + // // a = method kind. 5 bits. // b = method kind populated. 1 bit. // @@ -76,40 +78,42 @@ private struct PackedFlags // w = IsSetsRequiredMembersPopulated. 1 bit. // x = IsUnscopedRef. 1 bit. // y = IsUnscopedRefPopulated. 1 bit. - // 2 bits remain for future purposes. + // z = IsInterceptable. 1 bit. + // A = IsInterceptablePopulated. 1 bit. + // 31 bits remain for future purposes. private const int MethodKindOffset = 0; - private const int MethodKindMask = 0x1F; - - private const int MethodKindIsPopulatedBit = 0x1 << 5; - private const int IsExtensionMethodBit = 0x1 << 6; - private const int IsExtensionMethodIsPopulatedBit = 0x1 << 7; - private const int IsExplicitFinalizerOverrideBit = 0x1 << 8; - private const int IsExplicitClassOverrideBit = 0x1 << 9; - private const int IsExplicitOverrideIsPopulatedBit = 0x1 << 10; - private const int IsObsoleteAttributePopulatedBit = 0x1 << 11; - private const int IsCustomAttributesPopulatedBit = 0x1 << 12; - private const int IsUseSiteDiagnosticPopulatedBit = 0x1 << 13; - private const int IsConditionalPopulatedBit = 0x1 << 14; - private const int IsOverriddenOrHiddenMembersPopulatedBit = 0x1 << 15; - private const int IsReadOnlyBit = 0x1 << 16; - private const int IsReadOnlyPopulatedBit = 0x1 << 17; + private const long MethodKindMask = 0x1F; + + private const long MethodKindIsPopulatedBit = 0x1L << 5; + private const long IsExtensionMethodBit = 0x1L << 6; + private const long IsExtensionMethodIsPopulatedBit = 0x1L << 7; + private const long IsExplicitFinalizerOverrideBit = 0x1L << 8; + private const long IsExplicitClassOverrideBit = 0x1L << 9; + private const long IsExplicitOverrideIsPopulatedBit = 0x1L << 10; + private const long IsObsoleteAttributePopulatedBit = 0x1L << 11; + private const long IsCustomAttributesPopulatedBit = 0x1L << 12; + private const long IsUseSiteDiagnosticPopulatedBit = 0x1L << 13; + private const long IsConditionalPopulatedBit = 0x1L << 14; + private const long IsOverriddenOrHiddenMembersPopulatedBit = 0x1L << 15; + private const long IsReadOnlyBit = 0x1L << 16; + private const long IsReadOnlyPopulatedBit = 0x1L << 17; private const int NullableContextOffset = 18; - private const int NullableContextMask = 0x7; - private const int DoesNotReturnBit = 0x1 << 21; - private const int IsDoesNotReturnPopulatedBit = 0x1 << 22; - private const int IsMemberNotNullPopulatedBit = 0x1 << 23; - private const int IsInitOnlyBit = 0x1 << 24; - private const int IsInitOnlyPopulatedBit = 0x1 << 25; - private const int IsUnmanagedCallersOnlyAttributePopulatedBit = 0x1 << 26; - private const int HasSetsRequiredMembersBit = 0x1 << 27; - private const int HasSetsRequiredMembersPopulatedBit = 0x1 << 28; - private const int IsUnscopedRefBit = 0x1 << 29; - private const int IsUnscopedRefPopulatedBit = 0x1 << 30; - private const int IsInterceptableBit = 0x1 << 31; - private const int IsInterceptablePopulatedBit = 0x1 << 32; - - private int _bits; + private const long NullableContextMask = 0x7; + private const long DoesNotReturnBit = 0x1L << 21; + private const long IsDoesNotReturnPopulatedBit = 0x1L << 22; + private const long IsMemberNotNullPopulatedBit = 0x1L << 23; + private const long IsInitOnlyBit = 0x1L << 24; + private const long IsInitOnlyPopulatedBit = 0x1L << 25; + private const long IsUnmanagedCallersOnlyAttributePopulatedBit = 0x1L << 26; + private const long HasSetsRequiredMembersBit = 0x1L << 27; + private const long HasSetsRequiredMembersPopulatedBit = 0x1L << 28; + private const long IsUnscopedRefBit = 0x1L << 29; + private const long IsUnscopedRefPopulatedBit = 0x1L << 30; + private const long IsInterceptableBit = 0x1L << 31; + private const long IsInterceptablePopulatedBit = 0x1L << 32; + + private long _bits; public MethodKind MethodKind { @@ -120,8 +124,8 @@ public MethodKind MethodKind set { - Debug.Assert((int)value == ((int)value & MethodKindMask)); - _bits = (_bits & ~(MethodKindMask << MethodKindOffset)) | (((int)value & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; + Debug.Assert((long)value == ((long)value & MethodKindMask)); + _bits = (_bits & ~(MethodKindMask << MethodKindOffset)) | (((long)value & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; } } @@ -160,36 +164,36 @@ static PackedFlags() } #endif - private static bool BitsAreUnsetOrSame(int bits, int mask) + private static bool BitsAreUnsetOrSame(long bits, long mask) { return (bits & mask) == 0 || (bits & mask) == mask; } public void InitializeIsExtensionMethod(bool isExtensionMethod) { - int bitsToSet = (isExtensionMethod ? IsExtensionMethodBit : 0) | IsExtensionMethodIsPopulatedBit; + var bitsToSet = (isExtensionMethod ? IsExtensionMethodBit : 0) | IsExtensionMethodIsPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } public void InitializeIsReadOnly(bool isReadOnly) { - int bitsToSet = (isReadOnly ? IsReadOnlyBit : 0) | IsReadOnlyPopulatedBit; + var bitsToSet = (isReadOnly ? IsReadOnlyBit : 0) | IsReadOnlyPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } public void InitializeMethodKind(MethodKind methodKind) { - Debug.Assert((int)methodKind == ((int)methodKind & MethodKindMask)); - int bitsToSet = (((int)methodKind & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; + Debug.Assert((long)methodKind == ((long)methodKind & MethodKindMask)); + var bitsToSet = (((long)methodKind & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } public void InitializeIsExplicitOverride(bool isExplicitFinalizerOverride, bool isExplicitClassOverride) { - int bitsToSet = + var bitsToSet = (isExplicitFinalizerOverride ? IsExplicitFinalizerOverrideBit : 0) | (isExplicitClassOverride ? IsExplicitClassOverrideBit : 0) | IsExplicitOverrideIsPopulatedBit; @@ -229,12 +233,12 @@ public bool TryGetNullableContext(out byte? value) public bool SetNullableContext(byte? value) { - return ThreadSafeFlagOperations.Set(ref _bits, (((int)value.ToNullableContextFlags() & NullableContextMask) << NullableContextOffset)); + return ThreadSafeFlagOperations.Set(ref _bits, (((long)value.ToNullableContextFlags() & NullableContextMask) << NullableContextOffset)); } public bool InitializeDoesNotReturn(bool value) { - int bitsToSet = IsDoesNotReturnPopulatedBit; + var bitsToSet = IsDoesNotReturnPopulatedBit; if (value) bitsToSet |= DoesNotReturnBit; return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); @@ -247,7 +251,7 @@ public void SetIsMemberNotNullPopulated() public void InitializeIsInitOnly(bool isInitOnly) { - int bitsToSet = (isInitOnly ? IsInitOnlyBit : 0) | IsInitOnlyPopulatedBit; + var bitsToSet = (isInitOnly ? IsInitOnlyBit : 0) | IsInitOnlyPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } @@ -259,7 +263,7 @@ public void SetIsUnmanagedCallersOnlyAttributePopulated() public bool InitializeSetsRequiredMembersBit(bool value) { - int bitsToSet = HasSetsRequiredMembersPopulatedBit; + var bitsToSet = HasSetsRequiredMembersPopulatedBit; if (value) bitsToSet |= HasSetsRequiredMembersBit; return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); @@ -267,7 +271,7 @@ public bool InitializeSetsRequiredMembersBit(bool value) public bool InitializeIsUnscopedRef(bool value) { - int bitsToSet = IsUnscopedRefPopulatedBit; + var bitsToSet = IsUnscopedRefPopulatedBit; if (value) bitsToSet |= IsUnscopedRefBit; return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); @@ -275,7 +279,7 @@ public bool InitializeIsUnscopedRef(bool value) public bool InitializeInterceptable(bool value) { - int bitsToSet = IsInterceptablePopulatedBit; + var bitsToSet = IsInterceptablePopulatedBit; if (value) bitsToSet |= IsInterceptableBit; return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index e75b84987289e..bf0e7363f26bd 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -50,9 +50,11 @@ void verify(ModuleSymbol module) { var method = module.GlobalNamespace.GetMember("C.InterceptableMethod"); Assert.True(method.IsInterceptable); + Assert.Equal(MethodKind.Ordinary, method.MethodKind); method = module.GlobalNamespace.GetMember("C.NotInterceptable"); Assert.False(method.IsInterceptable); + Assert.Equal(MethodKind.Ordinary, method.MethodKind); } } diff --git a/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs b/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs index 8e0d40e161b1c..bbcf58ccdf578 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs @@ -56,11 +56,11 @@ internal static T[] GetValues() where T : struct } #if DEBUG - internal static bool ContainsAllValues(int mask) where T : struct, Enum, IConvertible + internal static bool ContainsAllValues(long mask) where T : struct, Enum, IConvertible { foreach (T value in GetValues()) { - int val = value.ToInt32(null); + long val = value.ToInt64(null); if ((val & mask) != val) { return false; diff --git a/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs b/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs index 536e48c2caba4..af304f9e051e5 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs @@ -24,6 +24,22 @@ public static bool Set(ref int flags, int toSet) return true; } + public static bool Set(ref long flags, long toSet) + { + long oldState, newState; + do + { + oldState = flags; + newState = oldState | toSet; + if (newState == oldState) + { + return false; + } + } + while (Interlocked.CompareExchange(ref flags, newState, oldState) != oldState); + return true; + } + public static bool Clear(ref int flags, int toClear) { int oldState, newState; From b7862a3cf63abcc772ef5121de2be6f9db4b111a Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 28 Mar 2023 18:02:49 -0700 Subject: [PATCH 24/49] Use 1-indexed line and character numbers in attribute --- .../InterceptsLocationAttributeData.cs | 4 +- .../SourceMethodSymbolWithAttributes.cs | 16 ++- .../Semantic/Semantics/InterceptorsTests.cs | 116 +++++++++--------- 3 files changed, 72 insertions(+), 64 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs index 0404a6b1f4064..746dbdea4d770 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/InterceptsLocationAttributeData.cs @@ -7,6 +7,8 @@ namespace Microsoft.CodeAnalysis.CSharp /// /// Information decoded from InterceptsLocationAttribute. /// - // PROTOTYPE(ic): record ok? + /// The 0-indexed line number. + /// The 0-indexed character number. + // PROTOTYPE(ic): move away from records internal sealed record InterceptsLocationAttributeData(string FilePath, int Line, int Character, Location AttributeLocation); } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 370a0f6ad80cb..650da4c662c87 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -949,13 +949,19 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments var attributeArguments = arguments.Attribute.CommonConstructorArguments; if (attributeArguments is not [ { Kind: not TypedConstantKind.Array, Value: string filePath }, - { Kind: not TypedConstantKind.Array, Value: int lineNumber }, - { Kind: not TypedConstantKind.Array, Value: int characterNumber }]) + { Kind: not TypedConstantKind.Array, Value: int displayLineNumber }, + { Kind: not TypedConstantKind.Array, Value: int displayCharacterNumber }]) { // Since the attribute does not have errors (asserted above), it should be guaranteed that we have the above arguments. throw ExceptionUtilities.Unreachable(); } + // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. + int lineNumber = displayLineNumber - 1; + int characterNumber = displayCharacterNumber - 1; + + // PROTOTYPE(ic): test with zero or negative display line/character numbers. + var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; var attributeLocation = arguments.AttributeSyntaxOpt.Location; if (Arity != 0 || ContainingType.IsGenericType) @@ -995,7 +1001,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments var referencedLineCount = referencedLines.Count; if (lineNumber >= referencedLineCount) { - diagnostics.Add(ErrorCode.ERR_InterceptorLineOutOfRange, attributeLocation, referencedLines.Count, lineNumber); + diagnostics.Add(ErrorCode.ERR_InterceptorLineOutOfRange, attributeLocation, referencedLineCount, displayLineNumber); return; } @@ -1003,7 +1009,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments var lineLength = line.End - line.Start; if (characterNumber >= lineLength) { - diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeLocation, lineLength, characterNumber); + diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeLocation, lineLength, displayCharacterNumber); return; } @@ -1026,7 +1032,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments if (tokenPositionDifference != 0) { // tokens don't span multiple lines, so we can apply the difference we found to the characterNumber within the line and figure out which character the user should have used. - diagnostics.Add(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, attributeLocation, referencedToken.Text, characterNumber - tokenPositionDifference); + diagnostics.Add(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, attributeLocation, referencedToken.Text, displayCharacterNumber - tokenPositionDifference); return; } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index bf0e7363f26bd..988191733e3ab 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -78,7 +78,7 @@ public static void Main() class D { - [InterceptsLocation("Program.cs", 10, 8)] + [InterceptsLocation("Program.cs", 11, 9)] public static void Interceptor1() { Console.Write("interceptor 1"); } } """; @@ -106,7 +106,7 @@ public static void Main() class D { - [InterceptsLocation("Program.cs", 10, 8)] + [InterceptsLocation("Program.cs", 11, 9)] private static void Interceptor1() { Console.Write("interceptor 1"); } } """; @@ -139,7 +139,7 @@ public static void Main() file class D { - [InterceptsLocation("Program.cs", 10, 8)] + [InterceptsLocation("Program.cs", 11, 9)] public static void Interceptor1() { Console.Write("interceptor 1"); } } """; @@ -173,7 +173,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; @@ -209,7 +209,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static C Interceptor1(this C i1, string param) { Console.Write("interceptor " + param); return i1; } } """; @@ -241,7 +241,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } } """; @@ -269,15 +269,15 @@ class C [Interceptable] public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } - [InterceptsLocation("Program.cs", 7, 10)] + [InterceptsLocation("Program.cs", 8, 11)] public void Interceptor1(string param) { Console.Write("interceptor " + param); } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // Program.cs(17,6): error CS27012: Interceptor must not have a 'this' parameter because 'C.InterceptableMethod(string)' does not have a 'this' parameter. - // [InterceptsLocation("Program.cs", 7, 10)] - Diagnostic(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, @"InterceptsLocation(""Program.cs"", 7, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(17, 6)); + // [InterceptsLocation("Program.cs", 8, 11)] + Diagnostic(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, @"InterceptsLocation(""Program.cs"", 8, 11)").WithArguments("C.InterceptableMethod(string)").WithLocation(17, 6)); } // 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. @@ -306,7 +306,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static void Interceptor1(this C c, string s1, string s2) { Console.Write("interceptor " + s1 + s2); } } """; @@ -339,7 +339,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; @@ -376,7 +376,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 8, 10)] + [InterceptsLocation("Program.cs", 9, 11)] public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } } """; @@ -447,7 +447,7 @@ public static void Main() static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } } - [InterceptsLocation("Program.cs", 10, 8)] + [InterceptsLocation("Program.cs", 11, 9)] public static void Interceptor1(string param) { Console.Write("interceptor " + param); } } """; @@ -481,15 +481,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 13, 10)] + [InterceptsLocation("Program.cs", 14, 11)] public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } } """; var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); verifier.VerifyDiagnostics( // Program.cs(20,6): warning CS27000: Cannot intercept a call to 'C.InterceptableMethod(string)' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - // [InterceptsLocation("Program.cs", 13, 10)] - Diagnostic(ErrorCode.WRN_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 13, 10)").WithArguments("C.InterceptableMethod(string)").WithLocation(20, 6) + // [InterceptsLocation("Program.cs", 14, 11)] + Diagnostic(ErrorCode.WRN_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 14, 11)").WithArguments("C.InterceptableMethod(string)").WithLocation(20, 6) ); } @@ -518,15 +518,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 15, 10)] + [InterceptsLocation("Program.cs", 16, 11)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(22,6): error CS27001: Method 'D.Interceptor1(I1, string)' cannot be used as an interceptor because it or its containing type has type parameters. - // [InterceptsLocation("Program.cs", 15, 10)] - Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 15, 10)").WithArguments("D.Interceptor1(I1, string)").WithLocation(22, 6)); + // Program.cs(22,6): error CS27001: Method 'D.Interceptor1(I1, string)' cannot be used as an interceptor because it or its containing type has type parameters. + // [InterceptsLocation("Program.cs", 16, 11)] + Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 16, 11)").WithArguments("D.Interceptor1(I1, string)").WithLocation(22, 6)); } [Fact] @@ -553,15 +553,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static void Interceptor1(string param) { Console.Write("interceptor " + param); } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // Program.cs(21,6): error CS27001: Method 'D.Interceptor1(string)' cannot be used as an interceptor because it or its containing type has type parameters. - // [InterceptsLocation("Program.cs", 14, 10)] - Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("D.Interceptor1(string)").WithLocation(21, 6)); + // [InterceptsLocation("Program.cs", 15, 11)] + Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 15, 11)").WithArguments("D.Interceptor1(string)").WithLocation(21, 6)); } [Fact] @@ -590,7 +590,7 @@ static class Outer { static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static void Interceptor1(string param) { Console.Write("interceptor " + param); } } } @@ -598,8 +598,8 @@ static class D var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // Program.cs(23,10): error CS27001: Method 'Outer.D.Interceptor1(string)' cannot be used as an interceptor because it or its containing type has type parameters. - // [InterceptsLocation("Program.cs", 14, 10)] - Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("Outer.D.Interceptor1(string)").WithLocation(23, 10) + // [InterceptsLocation("Program.cs", 15, 11)] + Diagnostic(ErrorCode.ERR_InterceptorCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 15, 11)").WithArguments("Outer.D.Interceptor1(string)").WithLocation(23, 10) ); } @@ -660,15 +660,15 @@ public static void Main() static class D { - [InterceptsLocation("BAD", 14, 10)] + [InterceptsLocation("BAD", 15, 11)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // Program.cs(21,6): error CS27002: Cannot intercept: compilation does not contain a file with path 'BAD'. - // [InterceptsLocation("BAD", 14, 10)] - Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"InterceptsLocation(""BAD"", 14, 10)").WithArguments("BAD").WithLocation(21, 6) + // [InterceptsLocation("BAD", 15, 11)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"InterceptsLocation(""BAD"", 15, 11)").WithArguments("BAD").WithLocation(21, 6) ); } @@ -696,15 +696,15 @@ public static void Main() static class D { - [InterceptsLocation("projects/Program.cs", 14, 10)] + [InterceptsLocation("projects/Program.cs", 15, 11)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "/Users/me/projects/Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // /Users/me/projects/Program.cs(21,6): error CS27003: Cannot intercept: compilation does not contain a file with path 'projects/Program.cs'. Did you mean to use path '/Users/me/projects/Program.cs'? - // [InterceptsLocation("projects/Program.cs", 14, 10)] - Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"InterceptsLocation(""projects/Program.cs"", 14, 10)").WithArguments("projects/Program.cs", "/Users/me/projects/Program.cs").WithLocation(21, 6) + // [InterceptsLocation("projects/Program.cs", 15, 11)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"InterceptsLocation(""projects/Program.cs"", 15, 11)").WithArguments("projects/Program.cs", "/Users/me/projects/Program.cs").WithLocation(21, 6) ); } @@ -768,7 +768,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 15, 1000)] + [InterceptsLocation("Program.cs", 16, 1000)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; @@ -776,7 +776,7 @@ static class D comp.VerifyEmitDiagnostics( // Program.cs(21,6): error CS27006: The given line is '5' characters long, which is fewer than the provided character number '1000'. // [InterceptsLocation("Program.cs", 15, 1000)] - Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 15, 1000)").WithArguments("5", "1000").WithLocation(21, 6) + Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 16, 1000)").WithArguments("5", "1000").WithLocation(21, 6) ); } @@ -804,15 +804,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 8)] + [InterceptsLocation("Program.cs", 15, 9)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable method, but rather to token 'c'. - // [InterceptsLocation("Program.cs", 14, 8)] - Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 8)").WithArguments("c").WithLocation(21, 6) + // [InterceptsLocation("Program.cs", 15, 9)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 15, 9)").WithArguments("c").WithLocation(21, 6) ); } @@ -840,15 +840,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 12)] + [InterceptsLocation("Program.cs", 15, 13)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(21,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '10' instead. - // [InterceptsLocation("Program.cs", 14, 12)] - Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 14, 12)").WithArguments("InterceptableMethod", "10").WithLocation(21, 6) + // Program.cs(21,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '11' instead. + // [InterceptsLocation("Program.cs", 15, 13)] + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 15, 13)").WithArguments("InterceptableMethod", "11").WithLocation(21, 6) ); } @@ -875,8 +875,8 @@ public static void Main() [Interceptable] public static C InterceptableMethod(this C c, string param) { Console.Write("interceptable " + param); return c; } - [InterceptsLocation("Program.cs", 11, 10)] // intercept spaces before 'InterceptableMethod' token - [InterceptsLocation("Program.cs", 13, 32)] // intercept spaces after 'InterceptableMethod' token + [InterceptsLocation("Program.cs", 12, 11)] // intercept spaces before 'InterceptableMethod' token + [InterceptsLocation("Program.cs", 14, 33)] // intercept spaces after 'InterceptableMethod' token public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } } @@ -886,12 +886,12 @@ static class CExt """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(20,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '12' instead. - // [InterceptsLocation("Program.cs", 11, 10)] // intercept spaces before 'InterceptableMethod' token - Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 11, 10)").WithArguments("InterceptableMethod", "12").WithLocation(20, 6), - // Program.cs(21,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '10' instead. - // [InterceptsLocation("Program.cs", 13, 32)] // intercept spaces after 'InterceptableMethod' token - Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 13, 32)").WithArguments("InterceptableMethod", "10").WithLocation(21, 6) + // Program.cs(20,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '13' instead. + // [InterceptsLocation("Program.cs", 12, 11)] // intercept spaces before 'InterceptableMethod' token + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 12, 11)").WithArguments("InterceptableMethod", "13").WithLocation(20, 6), + // Program.cs(21,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '11' instead. + // [InterceptsLocation("Program.cs", 14, 33)] // intercept spaces after 'InterceptableMethod' token + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 14, 33)").WithArguments("InterceptableMethod", "11").WithLocation(21, 6) ); } @@ -919,7 +919,7 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static I1 Interceptor1(this I1 i1, int param) { Console.Write("interceptor " + param); return i1; } } """; @@ -957,15 +957,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 15, 10)] + [InterceptsLocation("Program.cs", 16, 11)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // Program.cs(22,6): error CS27011: Interceptor must have a 'this' parameter matching parameter 'C this' on 'C.InterceptableMethod(string)'. - // [InterceptsLocation("Program.cs", 15, 10)] - Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 15, 10)").WithArguments("C this", "C.InterceptableMethod(string)").WithLocation(22, 6) + // [InterceptsLocation("Program.cs", 16, 11)] + Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 16, 11)").WithArguments("C this", "C.InterceptableMethod(string)").WithLocation(22, 6) ); } @@ -997,15 +997,15 @@ public static void Main() static class D { - [InterceptsLocation("Program.cs", 14, 10)] + [InterceptsLocation("Program.cs", 15, 11)] public static void Interceptor1(this S s, string param) { Console.Write("interceptor " + param); } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( // Program.cs(21,6): error CS27011: Interceptor must have a 'this' parameter matching parameter 'ref S this' on 'S.InterceptableMethod(string)'. - // [InterceptsLocation("Program.cs", 14, 10)] - Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 14, 10)").WithArguments("ref S this", "S.InterceptableMethod(string)").WithLocation(21, 6) + // [InterceptsLocation("Program.cs", 15, 11)] + Diagnostic(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, @"InterceptsLocation(""Program.cs"", 15, 11)").WithArguments("ref S this", "S.InterceptableMethod(string)").WithLocation(21, 6) ); } @@ -1034,7 +1034,7 @@ public void M([InterpolatedStringHandlerArgument("")] CustomHandler c) public static class S1Ext { - [InterceptsLocation("Program.cs", 4, 2)] + [InterceptsLocation("Program.cs", 5, 3)] public static void M1(ref this S1 s1, CustomHandler c) { Console.Write(2); @@ -1086,7 +1086,7 @@ public void M(CustomHandler c) public static class S1Ext { - [InterceptsLocation("Program.cs", 4, 2)] + [InterceptsLocation("Program.cs", 5, 3)] public static void M1(ref this S1 s1, [InterpolatedStringHandlerArgument("s1")] CustomHandler c) { Console.Write(1); From f9606801429d130b83d4c87c275ec1ae2c576747 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 28 Mar 2023 18:37:51 -0700 Subject: [PATCH 25/49] Add InterceptableLocation API and preview features attribute (tests pending) --- .../Portable/BoundTree/BoundExpression.cs | 12 +---- .../CSharp/Portable/CSharpExtensions.cs | 33 ++++++++++++- .../CSharp/Portable/PublicAPI.Unshipped.txt | 6 +++ .../Portable/Syntax/InterceptableLocation.cs | 39 +++++++++++++++ .../Utilities/RequiresPreviewFeatures.cs | 48 +++++++++++++++++++ 5 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs create mode 100644 src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index 64f0e1c48553f..5dbbd8ad9fc2a 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -249,17 +249,7 @@ public Location? InterceptableLocation return null; } - // If a qualified name is used as a valid receiver of an invocation syntax at some point, - // we probably want to treat it similarly to a MemberAccessExpression. - // However, we don't expect to encounter it. - Debug.Assert(syntax.Expression is not QualifiedNameSyntax); - - return syntax.Expression switch - { - MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Location, - SimpleNameSyntax name => name.Location, - _ => null - }; + return syntax.GetInterceptableLocationInternal(); } } } diff --git a/src/Compilers/CSharp/Portable/CSharpExtensions.cs b/src/Compilers/CSharp/Portable/CSharpExtensions.cs index 3e50a0db77a2c..d6d82250ba0e5 100644 --- a/src/Compilers/CSharp/Portable/CSharpExtensions.cs +++ b/src/Compilers/CSharp/Portable/CSharpExtensions.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; using System.Threading; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -389,7 +391,36 @@ internal static IList GetDirectives(this SyntaxNode node, { return ((CSharpSyntaxNode)node).GetLastDirective(predicate); } - #endregion + + internal static Location? GetInterceptableLocationInternal(this InvocationExpressionSyntax syntax) + { + // If a qualified name is used as a valid receiver of an invocation syntax at some point, + // we probably want to treat it similarly to a MemberAccessExpression. + // However, we don't expect to encounter it. + Debug.Assert(syntax.Expression is not QualifiedNameSyntax); + + return syntax.Expression switch + { + MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Location, + SimpleNameSyntax name => name.Location, + _ => null + }; + } + + [RequiresPreviewFeatures] + public static InterceptableLocation? GetInterceptableLocation(this InvocationExpressionSyntax syntax) + { + var location = syntax.GetInterceptableLocationInternal(); + if (location is null) + { + return null; + } + + var lineSpan = location.GetLineSpan().StartLinePosition; + return new InterceptableLocation(syntax.SyntaxTree.FilePath, lineSpan.Line + 1, lineSpan.Character + 1); + } + +#endregion #region SyntaxTree public static CompilationUnitSyntax GetCompilationUnitRoot(this SyntaxTree tree, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index 926b46346b6a8..ed3c43f590f46 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -1,5 +1,11 @@ *REMOVED*Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.Name.get -> Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax! *REMOVED*static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.RecordDeclaration(Microsoft.CodeAnalysis.SyntaxList attributeLists, Microsoft.CodeAnalysis.SyntaxTokenList modifiers, Microsoft.CodeAnalysis.SyntaxToken keyword, Microsoft.CodeAnalysis.SyntaxToken identifier, Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax! typeParameterList, Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax! parameterList, Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax! baseList, Microsoft.CodeAnalysis.SyntaxList constraintClauses, Microsoft.CodeAnalysis.SyntaxList members) -> Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax! +Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation +Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.CharacterNumber.get -> int +Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.FilePath.get -> string! +Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.InterceptableLocation() -> void +Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.LineNumber.get -> int +static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetInterceptableLocation(this Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax! syntax) -> Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation? static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.RecordDeclaration(Microsoft.CodeAnalysis.SyntaxList attributeLists, Microsoft.CodeAnalysis.SyntaxTokenList modifiers, Microsoft.CodeAnalysis.SyntaxToken keyword, Microsoft.CodeAnalysis.SyntaxToken identifier, Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax? typeParameterList, Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax? parameterList, Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax? baseList, Microsoft.CodeAnalysis.SyntaxList constraintClauses, Microsoft.CodeAnalysis.SyntaxList members) -> Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax! static Microsoft.CodeAnalysis.CSharpExtensions.ContainsDirective(this Microsoft.CodeAnalysis.SyntaxNode! node, Microsoft.CodeAnalysis.CSharp.SyntaxKind kind) -> bool Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.Name.get -> Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax? diff --git a/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs new file mode 100644 index 0000000000000..a49feea604a98 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.CSharp.Syntax +{ + /// + /// Contains values suitable for populating System.Runtime.CompilerServices.InterceptsLocationAttribute for a given call. + /// + [RequiresPreviewFeatures] + public struct InterceptableLocation + { + internal InterceptableLocation(string filePath, int lineNumber, int characterNumber) + { + FilePath = filePath; + LineNumber = lineNumber; + CharacterNumber = characterNumber; + } + + public string FilePath { get; } + + /// + /// The 1-indexed line number. + /// + public int LineNumber { get; } + + /// + /// The 1-indexed character number. + /// + public int CharacterNumber { get; } + } +} diff --git a/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs b/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs new file mode 100644 index 0000000000000..96a092ac6bda2 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.Versioning +{ + [AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Interface | + AttributeTargets.Delegate | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event, Inherited = false)] + internal sealed class RequiresPreviewFeaturesAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public RequiresPreviewFeaturesAttribute() { } + + /// + /// Initializes a new instance of the class with the specified message. + /// + /// An optional message associated with this attribute instance. + public RequiresPreviewFeaturesAttribute(string? message) + { + Message = message; + } + + /// + /// Returns the optional message associated with this attribute instance. + /// + public string? Message { get; } + + /// + /// Returns the optional URL associated with this attribute instance. + /// + public string? Url { get; set; } + } +} From 28bcf00f4d9848608e17ba1b901aadda2f1d49a8 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 28 Mar 2023 22:20:21 -0700 Subject: [PATCH 26/49] Don't declare attribute when building for netframework --- .../CSharp/Portable/Utilities/RequiresPreviewFeatures.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs b/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs index 96a092ac6bda2..02a3ea692787a 100644 --- a/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs +++ b/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs @@ -7,6 +7,8 @@ namespace System.Runtime.Versioning { +#if !NETCOREAPP + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | @@ -45,4 +47,6 @@ public RequiresPreviewFeaturesAttribute(string? message) /// public string? Url { get; set; } } + +#endif } From eae8165d2f4b9ae7acca4c9602359ae59ee0129b Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 28 Mar 2023 22:43:29 -0700 Subject: [PATCH 27/49] Fix formatting --- src/Compilers/CSharp/Portable/CSharpExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpExtensions.cs b/src/Compilers/CSharp/Portable/CSharpExtensions.cs index d6d82250ba0e5..b1550735ce5d4 100644 --- a/src/Compilers/CSharp/Portable/CSharpExtensions.cs +++ b/src/Compilers/CSharp/Portable/CSharpExtensions.cs @@ -419,8 +419,7 @@ internal static IList GetDirectives(this SyntaxNode node, var lineSpan = location.GetLineSpan().StartLinePosition; return new InterceptableLocation(syntax.SyntaxTree.FilePath, lineSpan.Line + 1, lineSpan.Character + 1); } - -#endregion + #endregion #region SyntaxTree public static CompilationUnitSyntax GetCompilationUnitRoot(this SyntaxTree tree, CancellationToken cancellationToken = default(CancellationToken)) From c1dbf7d22c724225128bcd017409f0adebb49c52 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 29 Mar 2023 16:09:14 -0700 Subject: [PATCH 28/49] Update doc --- docs/features/interceptors.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 1512acbb915e1..db24b712280e0 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -78,7 +78,8 @@ File paths used in `[InterceptsLocation]` must exactly match the paths on the sy The compiler does not map `#line` directives when determining if an `[InterceptsLocation]` attribute intercepts a particular call in syntax. #### Position -The implementation currently uses 0-indexed line and character numbers. However, we may want to change that before shipping it as an experimental feature to be 1-indexed, to match existing places where these values are displayed to the user (e.g. `Diagnostic.ToString`). + +Line and column numbers in `[InterceptsLocation]` are 1-indexed to match existing places where source locations are displayed to the user. For example, in `Diagnostic.ToString`. The location of the call is the location of the name syntax which denotes the interceptable method. For example, in `app.MapGet(...)`, the name syntax for `MapGet` would be considered the location of the call. For a static method call like `System.Console.WriteLine(...)`, the name syntax for `WriteLine` is the location of the call. If we allow intercepting calls to property accessors in the future (e.g `obj.Property`), we would also be able to use the name syntax in this way. From a89bd88c16770f3e98c2d012076688b90f95c2ee Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 29 Mar 2023 16:20:25 -0700 Subject: [PATCH 29/49] Clarify doc --- docs/features/interceptors.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index db24b712280e0..2631f1d777c1a 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -105,10 +105,9 @@ When a call is intercepted, the interceptor and interceptable methods must meet - When an interceptable instance method is compared to a classic extension method, we use the extension method in reduced form for comparison. The extension method parameter with the 'this' modifier is compared to the instance method 'this' parameter. - The returns and parameters, including the 'this' parameter, must have the same ref kinds and types, except that reference types with oblivious nullability can match either annotated or unannotated reference types. - Method names and parameter names are not required to match. -- Parameter default values are not required to match. +- Parameter default values are not required to match. When intercepting, default values on the interceptor method are ignored. - 'scoped' modifiers and `[UnscopedRefAttribute]` must be equivalent. - Arity does not need to match between intercepted and interceptor methods. In other words, it is permitted to intercept a generic method with a non-generic interceptor. ### Conflicting interceptors From 63ab62e51da2bb8618c53ec8a7164892a22fcd01 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 29 Mar 2023 17:32:27 -0700 Subject: [PATCH 30/49] Add implementation doc. Add a test. Add several PROTOTYPE test comments. --- docs/features/interceptors.md | 47 +++++++++++++++++- .../Semantic/Semantics/InterceptorsTests.cs | 48 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 2631f1d777c1a..ea2edc8fdf64f 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -71,6 +71,18 @@ namespace System.Runtime.CompilerServices } ``` +PROTOTYPE(ic): open question in https://github.com/dotnet/roslyn/pull/67432#discussion_r1147822738 + +Are the `[InterceptsLocation]` attributes dropped when emitting the methods to metadata? + +No. We could consider: + +* having the compiler drop the attributes automatically (treating them as pseudo-custom attributes, perhaps) +* making the attribute declaration in the framework "conditional" on some debug symbol which is not usually provided. +* deliberately keeping them there. I can't think of any good reason to do this. + +Perhaps the first option would be best. + #### File paths File paths used in `[InterceptsLocation]` must exactly match the paths on the syntax trees they refer to by ordinal comparison. `SyntaxTree.FilePath` has already applied `/pathmap` substitution, so the paths used in the attribute will be less environment-specific in many projects. @@ -81,13 +93,15 @@ The compiler does not map `#line` directives when determining if an `[Intercepts Line and column numbers in `[InterceptsLocation]` are 1-indexed to match existing places where source locations are displayed to the user. For example, in `Diagnostic.ToString`. -The location of the call is the location of the name syntax which denotes the interceptable method. For example, in `app.MapGet(...)`, the name syntax for `MapGet` would be considered the location of the call. For a static method call like `System.Console.WriteLine(...)`, the name syntax for `WriteLine` is the location of the call. If we allow intercepting calls to property accessors in the future (e.g `obj.Property`), we would also be able to use the name syntax in this way. +The location of the call is the location of the simple name syntax which denotes the interceptable method. For example, in `app.MapGet(...)`, the name syntax for `MapGet` would be considered the location of the call. For a static method call like `System.Console.WriteLine(...)`, the name syntax for `WriteLine` is the location of the call. If we allow intercepting calls to property accessors in the future (e.g `obj.Property`), we would also be able to use the name syntax in this way. #### Attribute creation The goal of the above decisions is to make it so that when source generators are filling in `[InterceptsLocation(...)]`, they simply need to read `nameSyntax.SyntaxTree.FilePath` and `nameSyntax.GetLineSpan().Span.Start` for the exact file path and position information they need to use. -We should provide samples of recommended coding patterns for generator authors, and perhaps provide public helper methods, which will make it relatively easy for them to do the right thing. For example, we could provide a helper on InvocationExpressionSyntax which returns the values that need to be put in `[InterceptsLocation]`. +We add the public API `public static InterceptableLocation? GetInterceptableLocation(this InvocationExpressionSyntax syntax)`. The API and the new return type are guarded by `[RequiresPreviewFeatures]`, requiring generators which use the APIs to opt-in. If the feature is significantly revised or withdrawn, use of `[RequiresPreviewFeatures]` gives us the latitude to make breaking changes to these public APIs or to remove them. + +We should provide samples of recommended coding patterns for generator authors. ### Non-invocation method usages @@ -123,3 +137,32 @@ An interceptor must be accessible at the location where interception is occurrin ### Editor experience Interceptors are treated like a post-compilation step in this design. Diagnostics are given for misuse of interceptors, but some diagnostics are only given in the command-line build and not in the IDE. There is limited traceability in the editor for which calls in a compilation are actually being intercepted. If this feature is brought forward past the experimental stage, this limitation will need to be re-examined. + +### User opt-in + +PROTOTYPE(ic): design an opt-in step which meets the needs of the generators consuming this, and permits usage of the feature in the key NativeAOT scenarios we are targeting. + +As interceptors are an experimental feature, it's important that an explicit opt-in step be performed by the user in order to depend on it. + +### Implementation strategy + +During the binding phase, `InterceptsLocationAttribute` usages are decoded and the related data for each usage are collected in a `ConcurrentSet` on the compilation: +- attribute arguments +- attribute location +- attributed method symbol +PROTOTYPE(ic): the exact collection used to collect the attribute usages, and the exact way it is used, are not finalized. The main concern is to ensure we can scale to large numbers of interceptors without issue, and that we can report diagnostics for duplicate interception of the same location in a deterministic way. + +At this time, diagnostics are reported for the following conditions: +- problems specific to the attributed interceptor method itself, for example, that it is not an ordinary method. +- syntactic problems specific to the referenced location, for example, that it does not refer to an applicable simple name as defined in [Position](#position) subsection. + +During the lowering phase, when a given `BoundCall` is lowered: +- we check if its syntax contains an applicable simple name +- if so, we lookup whether it is being intercepted, based on data about `InterceptsLocationAttribute` collected during the binding phase. +- if it is being intercepted, we perform an additional step after lowering of the receiver and arguments is completed: + - substitute the interceptable method with the interceptor method on the `BoundCall`. + - if the interceptor is a classic extension method, and the interceptable method is an instance method, we adjust the `BoundCall` to use the receiver as the first argument of the call, "pushing" the other arguments forward, similar to the way it would have bound if the original call were to an extension method in reduced form. + +At this time, diagnostics are reported for the following conditions: +- incompatibility between the interceptor and interceptable methods, for example, in their signatures. +- *duplicate* `[InterceptsLocation]`, that is, multiple interceptors which intercept the same call. PROTOTYPE(ic): not yet implemented. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 988191733e3ab..dffe8fa340add 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -184,6 +184,21 @@ static class D // PROTOTYPE(ic): test a case where the original method has type parameter constraints. // PROTOTYPE(ic): for now we will just completely disallow type parameters in the interceptor. // PROTOTYPE(ic): test where interceptable is a constructed method or a retargeting method. + // PROTOTYPE(ic): test where there are differences between 'scoped' modifiers and '[UnscopedRef]' attributes + + // PROTOTYPE(ic): Possible test cases: + // + // * Intercept instance method with instance method in same class, base class, derived class + // * Intercept with extern method + // * Intercept with abstract or interface 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 + // * Intercept a generic method call when the type parameters are / are not substituted + + [Fact] public void InterceptableInstanceMethod_InterceptorExtensionMethod() @@ -217,6 +232,38 @@ static class D verifier.VerifyDiagnostics(); } + [Fact] + public void QualifiedNameAtCallSite() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static C InterceptableMethod(C c, string param) { Console.Write("interceptable " + param); return c; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + C.InterceptableMethod(c, "call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 11)] + public static C Interceptor1(C c, string param) { Console.Write("interceptor " + param); return c; } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); + verifier.VerifyDiagnostics(); + } + [Fact] public void InterceptableStaticMethod_InterceptorExtensionMethod() { @@ -282,6 +329,7 @@ class C // 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. + // PROTOTYPE(ic): test when parameter names differ between interceptable and interceptor. [Fact] public void ArgumentLabels() { From 0243430f7a9a9833c1d93a582c96b138ed03eb89 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 29 Mar 2023 21:30:09 -0700 Subject: [PATCH 31/49] Fix formatting --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index dffe8fa340add..54e30536a44ef 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -198,8 +198,6 @@ static class D // * An explicit interface implementation marked as interceptable // * Intercept a generic method call when the type parameters are / are not substituted - - [Fact] public void InterceptableInstanceMethod_InterceptorExtensionMethod() { From 35b04d0b6d1bf4b9d892d389adf4071fae027547 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 30 Mar 2023 11:56:26 -0700 Subject: [PATCH 32/49] Address some feedback --- .../Lowering/LocalRewriter/LocalRewriter_Call.cs | 9 ++++++--- .../Portable/Symbols/NativeIntegerTypeSymbol.cs | 2 ++ .../Symbols/Retargeting/RetargetingMethodSymbol.cs | 2 ++ .../Portable/Symbols/SubstitutedMethodSymbol.cs | 2 ++ .../Portable/Symbols/Wrapped/WrappedMethodSymbol.cs | 8 -------- .../Core/Portable/InternalUtilities/EnumUtilties.cs | 13 +++++++++++++ 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index cc09e663884ad..04ae5774a6a37 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -221,9 +221,12 @@ public override BoundNode VisitCall(BoundCall node) { Debug.Assert(node != null); - // PROTOTYPE(ic): interception should occur after lowering - // so that we use the original method in all relevant situations such as interpolated string lowering. - var (method, receiverOpt, arguments, argsToParamsOpt, argRefKindsOpt, invokedAsExtensionMethod) = (node.Method, node.ReceiverOpt, node.Arguments, node.ArgsToParamsOpt, node.ArgumentRefKindsOpt, node.InvokedAsExtensionMethod); + MethodSymbol method = node.Method; + BoundExpression? receiverOpt = node.ReceiverOpt; + ImmutableArray arguments = node.Arguments; + ImmutableArray argsToParamsOpt = node.ArgsToParamsOpt; + ImmutableArray argRefKindsOpt = node.ArgumentRefKindsOpt; + bool invokedAsExtensionMethod = node.InvokedAsExtensionMethod; // Rewrite the receiver BoundExpression? rewrittenReceiver = VisitExpression(receiverOpt); diff --git a/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs index 7a296e86a1722..5b93f8268edd8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs @@ -349,6 +349,8 @@ public override ImmutableArray Parameters internal override UnmanagedCallersOnlyAttributeData? GetUnmanagedCallersOnlyAttributeData(bool forceComplete) => UnderlyingMethod.GetUnmanagedCallersOnlyAttributeData(forceComplete); + internal override bool IsInterceptable => UnderlyingMethod.IsInterceptable; + public override Symbol? AssociatedSymbol => _associatedSymbol; internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree localTree) diff --git a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs index 781406109136f..30d17e493ca9b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs @@ -256,6 +256,8 @@ public override ImmutableArray GetReturnTypeAttributes() return _lazyUnmanagedAttributeData; } + internal override bool IsInterceptable => UnderlyingMethod.IsInterceptable; + internal override bool TryGetThisParameter(out ParameterSymbol? thisParameter) { if (!_underlyingMethod.TryGetThisParameter(out var underlyingParameter)) diff --git a/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs index 4e21bb16912d1..81740b40b5cf2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs @@ -215,6 +215,8 @@ public override ImmutableArray GetReturnTypeAttributes() internal sealed override UnmanagedCallersOnlyAttributeData GetUnmanagedCallersOnlyAttributeData(bool forceComplete) => this.OriginalDefinition.GetUnmanagedCallersOnlyAttributeData(forceComplete); + internal sealed override bool IsInterceptable => UnderlyingMethod.IsInterceptable; + public sealed override Symbol AssociatedSymbol { get diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs index 3e463e1ed9982..736c6fdbd9ed1 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs @@ -186,14 +186,6 @@ public override bool IsImplicitlyDeclared } } - internal override bool IsInterceptable - { - get - { - return UnderlyingMethod.IsInterceptable; - } - } - internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) { return UnderlyingMethod.IsMetadataVirtual(ignoreInterfaceImplementationChanges); diff --git a/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs b/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs index bbcf58ccdf578..75a0010c53887 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/EnumUtilties.cs @@ -56,6 +56,19 @@ internal static T[] GetValues() where T : struct } #if DEBUG + internal static bool ContainsAllValues(int mask) where T : struct, Enum, IConvertible + { + foreach (T value in GetValues()) + { + int val = value.ToInt32(null); + if ((val & mask) != val) + { + return false; + } + } + return true; + } + internal static bool ContainsAllValues(long mask) where T : struct, Enum, IConvertible { foreach (T value in GetValues()) From b2de0a7f6e0722cbcf9cbded383d82a73b5993be Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 30 Mar 2023 14:07:11 -0700 Subject: [PATCH 33/49] Fix EE --- .../ExpressionCompiler/Symbols/EECompilationContextMethod.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs index 248c0d64c06a7..80cae2ca4740a 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs @@ -78,6 +78,8 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l return _underlyingMethod.GetUnmanagedCallersOnlyAttributeData(forceComplete); } + internal override bool IsInterceptable => _underlyingMethod.IsInterceptable; + internal override bool IsNullableAnalysisEnabled() { return _underlyingMethod.IsNullableAnalysisEnabled(); From a5a0a7e5c97524d42cc27deee08a5281534ecb1e Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 3 Apr 2023 15:21:34 -0700 Subject: [PATCH 34/49] Update docs/features/interceptors.md Co-authored-by: Charles Stoner <10732005+cston@users.noreply.github.com> --- docs/features/interceptors.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index ea2edc8fdf64f..2e9861217819d 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -116,11 +116,11 @@ Interceptors cannot have type parameters or be declared in generic types at any ### Signature matching When a call is intercepted, the interceptor and interceptable methods must meet the signature matching requirements detailed below: -- When an interceptable instance method is compared to a classic extension method, we use the extension method in reduced form for comparison. The extension method parameter with the 'this' modifier is compared to the instance method 'this' parameter. -- The returns and parameters, including the 'this' parameter, must have the same ref kinds and types, except that reference types with oblivious nullability can match either annotated or unannotated reference types. +- When an interceptable instance method is compared to a classic extension method, we use the extension method in reduced form for comparison. The extension method parameter with the `this` modifier is compared to the instance method `this` parameter. +- The returns and parameters, including the `this` parameter, must have the same ref kinds and types, except that reference types with oblivious nullability can match either annotated or unannotated reference types. - Method names and parameter names are not required to match. - Parameter default values are not required to match. When intercepting, default values on the interceptor method are ignored. -- 'scoped' modifiers and `[UnscopedRefAttribute]` must be equivalent. +- `scoped` modifiers and `[UnscopedRefAttribute]` must be equivalent. Arity does not need to match between intercepted and interceptor methods. In other words, it is permitted to intercept a generic method with a non-generic interceptor. From 9d1c07b19a1f799319287991e5ec355766b1687e Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 3 Apr 2023 16:14:16 -0700 Subject: [PATCH 35/49] Address some feedback --- docs/features/interceptors.md | 6 +- .../LocalRewriter/LocalRewriter_Call.cs | 4 + .../Portable/Syntax/InterceptableLocation.cs | 7 +- .../Utilities/RequiresPreviewFeatures.cs | 3 - .../Semantic/Semantics/InterceptorsTests.cs | 76 +++++++++++++++++++ .../Symbols/EECompilationContextMethod.cs | 2 +- 6 files changed, 87 insertions(+), 11 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 2e9861217819d..641a825c842be 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -132,7 +132,11 @@ If an `[InterceptsLocation]` attribute is found in the compilation which does no ### Interceptor accessibility -An interceptor must be accessible at the location where interception is occurring. +An interceptor must be accessible at the location where interception is occurring. PROTOTYPE(ic): This enforcement is not yet implemented. + +We imagine it will be common to want to discourage explicit use of interceptor methods. For this use case, should consider adjusting behavior of `[EditorBrowsable]` to work in the same compilation, and encouraging generator authors to use it to prevent interceptors from appearing in lookup, etc. + +PROTOTYPE(ic): Generators often want to put things not intended to be user-visible in fire-local types. This reduces the need to defend against name conflicts with other types in the user's project. A file-local type is not present in lookup outside of the file it is declared in. But, the type is *accessible* from the runtime point of view presently. Should we permit interceptors declared in file types to refer to other files? ### Editor experience diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 04ae5774a6a37..4d1aa43c51756 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -141,6 +141,10 @@ public void InterceptCallAndAdjustArguments( ref bool invokedAsExtensionMethod, Location? interceptableLocation) { + // PROTOTYPE(ic): + // 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.GetInterceptor(interceptableLocation) is not var (interceptsLocationAttributeData, interceptor)) { // The call was not intercepted. diff --git a/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs index a49feea604a98..fc271b72d1e78 100644 --- a/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs @@ -2,12 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Versioning; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.CodeAnalysis.CSharp.Syntax { @@ -15,7 +10,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Syntax /// Contains values suitable for populating System.Runtime.CompilerServices.InterceptsLocationAttribute for a given call. /// [RequiresPreviewFeatures] - public struct InterceptableLocation + public readonly struct InterceptableLocation { internal InterceptableLocation(string filePath, int lineNumber, int characterNumber) { diff --git a/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs b/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs index 02a3ea692787a..4a1e1a0281079 100644 --- a/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs +++ b/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs @@ -2,9 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - namespace System.Runtime.Versioning { #if !NETCOREAPP diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 54e30536a44ef..86317f77cf50c 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -649,6 +649,82 @@ static class D ); } + [Fact] + public void InterceptableGeneric_01() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod(T1 t) { Console.Write("0"); } + } + + static class Program + { + public static void Main() + { + C.InterceptableMethod("1"); + C.InterceptableMethod("2"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 11)] + [InterceptsLocation("Program.cs", 15, 11)] + public static void Interceptor1(string s) { Console.Write(s); } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "12"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InterceptableGeneric_02() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod(T t) { Console.Write("0"); } + } + + static class Program + { + public static void Main() + { + C.InterceptableMethod("1"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 14, 30)] + [InterceptsLocation("Program.cs", 14, 31)] + [InterceptsLocation("Program.cs", 14, 37)] + public static void Interceptor1(string s) { Console.Write(s); } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(20,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '<'. + // [InterceptsLocation("Program.cs", 14, 30)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 30)").WithArguments("<").WithLocation(20, 6), + // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token 'string'. + // [InterceptsLocation("Program.cs", 14, 31)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 31)").WithArguments("string").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 '>'. + // [InterceptsLocation("Program.cs", 14, 37)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 14, 37)").WithArguments(">").WithLocation(22, 6) + ); + } + [Fact] public void InterceptsLocationBadAttributeArguments_01() { diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs index 80cae2ca4740a..adf9b95a8d566 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs @@ -78,7 +78,7 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l return _underlyingMethod.GetUnmanagedCallersOnlyAttributeData(forceComplete); } - internal override bool IsInterceptable => _underlyingMethod.IsInterceptable; + internal override bool IsInterceptable => false; internal override bool IsNullableAnalysisEnabled() { From f89f983a09a23e4cf53d301bffdbae1633ac6cef Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 3 Apr 2023 18:30:14 -0700 Subject: [PATCH 36/49] Fix test --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 86317f77cf50c..d8958805f3cca 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -659,7 +659,7 @@ public void InterceptableGeneric_01() class C { [Interceptable] - public static void InterceptableMethod(T1 t) { Console.Write("0"); } + public static void InterceptableMethod(T t) { Console.Write("0"); } } static class Program From 571e69bf8fe997c9673d3f64879117054be47cfa Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 4 Apr 2023 00:14:18 -0700 Subject: [PATCH 37/49] Update docs/features/interceptors.md --- docs/features/interceptors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 641a825c842be..f39b1928d79af 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -136,7 +136,7 @@ An interceptor must be accessible at the location where interception is occurrin We imagine it will be common to want to discourage explicit use of interceptor methods. For this use case, should consider adjusting behavior of `[EditorBrowsable]` to work in the same compilation, and encouraging generator authors to use it to prevent interceptors from appearing in lookup, etc. -PROTOTYPE(ic): Generators often want to put things not intended to be user-visible in fire-local types. This reduces the need to defend against name conflicts with other types in the user's project. A file-local type is not present in lookup outside of the file it is declared in. But, the type is *accessible* from the runtime point of view presently. Should we permit interceptors declared in file types to refer to other files? +PROTOTYPE(ic): Generators often want to put things not intended to be user-visible in file-local types. This reduces the need to defend against name conflicts with other types in the user's project. A file-local type is not present in lookup outside of the file it is declared in. But, the type is *accessible* from the runtime point of view presently. Should we permit interceptors declared in file types to refer to other files? ### Editor experience From 336d29f76673b87b76406df1566adc4965bce732 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 4 Apr 2023 12:38:14 -0700 Subject: [PATCH 38/49] Add PROTOTYPE comment for flags code --- .../CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs index 26eac336d3961..7c4ce8ce1a02d 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs @@ -82,6 +82,8 @@ private struct PackedFlags // A = IsInterceptablePopulated. 1 bit. // 31 bits remain for future purposes. + // PROTOTYPE(ic): consider if we can move away from a 'long' for these flags and use less space. + private const int MethodKindOffset = 0; private const long MethodKindMask = 0x1F; From c16a87d4c58a8f6e64f5f6fbb7b885201c7c8dc0 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 4 Apr 2023 16:08:13 -0700 Subject: [PATCH 39/49] wip --- docs/features/interceptors.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index f39b1928d79af..70359a7a7d21d 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -3,7 +3,9 @@ ## Summary [summary]: #summary -*Interceptors* are an experimental compiler feature. An *interceptor* is a method which can declaratively substitute a call to an *interceptable* method with a call to itself at compile time. This substitution occurs by having the interceptor declare the source locations of the calls that it intercepts. This provides a limited facility to change the semantics of existing code by adding new code to a compilation (e.g. in a source generator). +*Interceptors* are an experimental compiler feature. + +An *interceptor* is a method which can declaratively substitute a call to an *interceptable* method with a call to itself at compile time. This substitution occurs by having the interceptor declare the source locations of the calls that it intercepts. This provides a limited facility to change the semantics of existing code by adding new code to a compilation (e.g. in a source generator). ```cs using System; @@ -146,7 +148,11 @@ Interceptors are treated like a post-compilation step in this design. Diagnostic PROTOTYPE(ic): design an opt-in step which meets the needs of the generators consuming this, and permits usage of the feature in the key NativeAOT scenarios we are targeting. -As interceptors are an experimental feature, it's important that an explicit opt-in step be performed by the user in order to depend on it. +Because this is a generator-oriented feature, we'd like to have an opt-in step during the *experimental* phase which permits generators to opt-in without the end user needing to take additional steps. + +Question: can a generator opt-in to a preview feature on behalf of the user? If not, is it fine to ask the user to take an additional step here? + +There's a concern on the ASP.NET side that if the user needs to opt-in to something which is "preview/experimental" then they will need to use their non-interceptors codegen strategy *as well as* the interceptors one for NativeAOT, depending on whether the user did the gesture to enable the feature. ### Implementation strategy From fd95acaa64332fed389dba1c2f080ff4fdb2badc Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 4 Apr 2023 16:08:27 -0700 Subject: [PATCH 40/49] more --- .../Symbols/Metadata/PE/PEMethodSymbol.cs | 7 ++- .../Semantic/Semantics/InterceptorsTests.cs | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs index 7c4ce8ce1a02d..67a870903fa9c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs @@ -82,7 +82,12 @@ private struct PackedFlags // A = IsInterceptablePopulated. 1 bit. // 31 bits remain for future purposes. - // PROTOTYPE(ic): consider if we can move away from a 'long' for these flags and use less space. + // PROTOTYPE(ic): consider reverting back to 'int' for these flags. + // - There may not be a need to cache 'IsInterceptable' on the symbol. We could just look it up from attributes each time. + // Since the number of calls which are intercepted is relatively small, this may be a negligible additional cost. + // - Otherwise, there may be more space efficient method of storing the flags which we should be using instead. + // For example, writing a byte is atomic, so if multiple *bits of information* aren't being stored independently in it, + // we could store 'IsInterceptable' in a 'ThreeState' field, for example. private const int MethodKindOffset = 0; private const long MethodKindMask = 0x1F; diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index d8958805f3cca..6ace9ed689fc7 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -1234,4 +1234,57 @@ public CustomHandler(int literalLength, int formattedCount, S1 s) expectedOutput: "1"); verifier.VerifyDiagnostics(); } + + [Fact] + public void InterpolatedStringHandler_03() + { + // Verify that interpolated string attributes on an interceptor don't cause us to somehow pick a different argument. + var code = """ +using System; +using System.Runtime.CompilerServices; + +var s1 = new S1(1); +var s2 = new S1(2); +S1.M(s1, s2, $""); + +public struct S1 +{ + public S1(int field) => Field = field; + public int Field = 1; + + [Interceptable] + public static void M(S1 s1, S1 s2, [InterpolatedStringHandlerArgument("s1")] CustomHandler c) + { + Console.Write(0); + } +} + +public static class S1Ext +{ + [InterceptsLocation("Program.cs", 6, 4)] + public static void M1(S1 s2, S1 s3, [InterpolatedStringHandlerArgument("s2")] CustomHandler c) + { + Console.Write(2); + } +} + +partial struct CustomHandler +{ + public CustomHandler(int literalLength, int formattedCount, S1 s) + { + Console.Write(s.Field); + } +} +"""; + var verifier = CompileAndVerify( + new[] + { + (code, "Program.cs"), + (InterpolatedStringHandlerArgumentAttribute, "a.cs"), + (GetInterpolatedStringCustomHandlerType("CustomHandler", "partial struct", useBoolReturns: false), "b.cs"), + s_attributesSource + }, + expectedOutput: "12"); + verifier.VerifyDiagnostics(); + } } From 7b7c38d474f4506b74727529f4c866d8fb727ba2 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 5 Apr 2023 11:18:12 -0700 Subject: [PATCH 41/49] add line directive tests --- .../Semantic/Semantics/InterceptorsTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 6ace9ed689fc7..88ea94aaed7ef 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -1287,4 +1287,67 @@ public CustomHandler(int literalLength, int formattedCount, S1 s) expectedOutput: "12"); verifier.VerifyDiagnostics(); } + + [Fact] + public void LineDirective_01() + { + // Verify that line directives are not considered when deciding if a particular call is being intercepted. + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod() { Console.Write("interceptable"); } + + public static void Main() + { + #line 42 "OtherFile.cs" + InterceptableMethod(); + } + } + + class D + { + [InterceptsLocation("Program.cs", 12, 9)] + public static void Interceptor1() { Console.Write("interceptor 1"); } + } + """; + var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor 1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void LineDirective_02() + { + // Verify that line directives are not considered when deciding if a particular call is being intercepted. + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public static void InterceptableMethod() { Console.Write("interceptable"); } + + public static void Main() + { + #line 42 "OtherFile.cs" + InterceptableMethod(); + } + } + + class D + { + [InterceptsLocation("OtherFile.cs", 42, 9)] + public static void Interceptor1() { Console.Write("interceptor 1"); } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // OtherFile.cs(48,6): error CS27002: Cannot intercept: compilation does not contain a file with path 'OtherFile.cs'. + // [InterceptsLocation("OtherFile.cs", 42, 9)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"InterceptsLocation(""OtherFile.cs"", 42, 9)").WithArguments("OtherFile.cs").WithLocation(48, 6)); + } } From a5792474614a08e901bd803bf5261c6607679d9e Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 5 Apr 2023 15:23:58 -0700 Subject: [PATCH 42/49] Drop helper API per working group feedback --- docs/features/interceptors.md | 4 +- .../Portable/BoundTree/BoundExpression.cs | 12 ++++- .../CSharp/Portable/CSharpExtensions.cs | 30 ------------ .../CSharp/Portable/PublicAPI.Unshipped.txt | 6 --- .../Portable/Syntax/InterceptableLocation.cs | 34 ------------- .../Utilities/RequiresPreviewFeatures.cs | 49 ------------------- 6 files changed, 12 insertions(+), 123 deletions(-) delete mode 100644 src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs delete mode 100644 src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 70359a7a7d21d..8abbdc0e6d1f4 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -101,9 +101,7 @@ The location of the call is the location of the simple name syntax which denotes The goal of the above decisions is to make it so that when source generators are filling in `[InterceptsLocation(...)]`, they simply need to read `nameSyntax.SyntaxTree.FilePath` and `nameSyntax.GetLineSpan().Span.Start` for the exact file path and position information they need to use. -We add the public API `public static InterceptableLocation? GetInterceptableLocation(this InvocationExpressionSyntax syntax)`. The API and the new return type are guarded by `[RequiresPreviewFeatures]`, requiring generators which use the APIs to opt-in. If the feature is significantly revised or withdrawn, use of `[RequiresPreviewFeatures]` gives us the latitude to make breaking changes to these public APIs or to remove them. - -We should provide samples of recommended coding patterns for generator authors. +We should provide samples of recommended coding patterns for generator authors to show correct usage of these, including the "translation" from 0-indexed to 1-indexed positions. ### Non-invocation method usages diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index 5dbbd8ad9fc2a..76f61c50ecd45 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -248,8 +248,18 @@ public Location? InterceptableLocation { return null; } + + // If a qualified name is used as a valid receiver of an invocation syntax at some point, + // we probably want to treat it similarly to a MemberAccessExpression. + // However, we don't expect to encounter it. + Debug.Assert(syntax.Expression is not QualifiedNameSyntax); - return syntax.GetInterceptableLocationInternal(); + return syntax.Expression switch + { + MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Location, + SimpleNameSyntax name => name.Location, + _ => null + }; } } } diff --git a/src/Compilers/CSharp/Portable/CSharpExtensions.cs b/src/Compilers/CSharp/Portable/CSharpExtensions.cs index b1550735ce5d4..3e50a0db77a2c 100644 --- a/src/Compilers/CSharp/Portable/CSharpExtensions.cs +++ b/src/Compilers/CSharp/Portable/CSharpExtensions.cs @@ -5,9 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.Versioning; using System.Threading; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -391,34 +389,6 @@ internal static IList GetDirectives(this SyntaxNode node, { return ((CSharpSyntaxNode)node).GetLastDirective(predicate); } - - internal static Location? GetInterceptableLocationInternal(this InvocationExpressionSyntax syntax) - { - // If a qualified name is used as a valid receiver of an invocation syntax at some point, - // we probably want to treat it similarly to a MemberAccessExpression. - // However, we don't expect to encounter it. - Debug.Assert(syntax.Expression is not QualifiedNameSyntax); - - return syntax.Expression switch - { - MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Location, - SimpleNameSyntax name => name.Location, - _ => null - }; - } - - [RequiresPreviewFeatures] - public static InterceptableLocation? GetInterceptableLocation(this InvocationExpressionSyntax syntax) - { - var location = syntax.GetInterceptableLocationInternal(); - if (location is null) - { - return null; - } - - var lineSpan = location.GetLineSpan().StartLinePosition; - return new InterceptableLocation(syntax.SyntaxTree.FilePath, lineSpan.Line + 1, lineSpan.Character + 1); - } #endregion #region SyntaxTree diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index ed3c43f590f46..926b46346b6a8 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -1,11 +1,5 @@ *REMOVED*Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.Name.get -> Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax! *REMOVED*static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.RecordDeclaration(Microsoft.CodeAnalysis.SyntaxList attributeLists, Microsoft.CodeAnalysis.SyntaxTokenList modifiers, Microsoft.CodeAnalysis.SyntaxToken keyword, Microsoft.CodeAnalysis.SyntaxToken identifier, Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax! typeParameterList, Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax! parameterList, Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax! baseList, Microsoft.CodeAnalysis.SyntaxList constraintClauses, Microsoft.CodeAnalysis.SyntaxList members) -> Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax! -Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation -Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.CharacterNumber.get -> int -Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.FilePath.get -> string! -Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.InterceptableLocation() -> void -Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation.LineNumber.get -> int -static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetInterceptableLocation(this Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax! syntax) -> Microsoft.CodeAnalysis.CSharp.Syntax.InterceptableLocation? static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.RecordDeclaration(Microsoft.CodeAnalysis.SyntaxList attributeLists, Microsoft.CodeAnalysis.SyntaxTokenList modifiers, Microsoft.CodeAnalysis.SyntaxToken keyword, Microsoft.CodeAnalysis.SyntaxToken identifier, Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax? typeParameterList, Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax? parameterList, Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax? baseList, Microsoft.CodeAnalysis.SyntaxList constraintClauses, Microsoft.CodeAnalysis.SyntaxList members) -> Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax! static Microsoft.CodeAnalysis.CSharpExtensions.ContainsDirective(this Microsoft.CodeAnalysis.SyntaxNode! node, Microsoft.CodeAnalysis.CSharp.SyntaxKind kind) -> bool Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.Name.get -> Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax? diff --git a/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs deleted file mode 100644 index fc271b72d1e78..0000000000000 --- a/src/Compilers/CSharp/Portable/Syntax/InterceptableLocation.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Runtime.Versioning; - -namespace Microsoft.CodeAnalysis.CSharp.Syntax -{ - /// - /// Contains values suitable for populating System.Runtime.CompilerServices.InterceptsLocationAttribute for a given call. - /// - [RequiresPreviewFeatures] - public readonly struct InterceptableLocation - { - internal InterceptableLocation(string filePath, int lineNumber, int characterNumber) - { - FilePath = filePath; - LineNumber = lineNumber; - CharacterNumber = characterNumber; - } - - public string FilePath { get; } - - /// - /// The 1-indexed line number. - /// - public int LineNumber { get; } - - /// - /// The 1-indexed character number. - /// - public int CharacterNumber { get; } - } -} diff --git a/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs b/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs deleted file mode 100644 index 4a1e1a0281079..0000000000000 --- a/src/Compilers/CSharp/Portable/Utilities/RequiresPreviewFeatures.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace System.Runtime.Versioning -{ -#if !NETCOREAPP - - [AttributeUsage(AttributeTargets.Assembly | - AttributeTargets.Module | - AttributeTargets.Class | - AttributeTargets.Interface | - AttributeTargets.Delegate | - AttributeTargets.Struct | - AttributeTargets.Enum | - AttributeTargets.Constructor | - AttributeTargets.Method | - AttributeTargets.Property | - AttributeTargets.Field | - AttributeTargets.Event, Inherited = false)] - internal sealed class RequiresPreviewFeaturesAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public RequiresPreviewFeaturesAttribute() { } - - /// - /// Initializes a new instance of the class with the specified message. - /// - /// An optional message associated with this attribute instance. - public RequiresPreviewFeaturesAttribute(string? message) - { - Message = message; - } - - /// - /// Returns the optional message associated with this attribute instance. - /// - public string? Message { get; } - - /// - /// Returns the optional URL associated with this attribute instance. - /// - public string? Url { get; set; } - } - -#endif -} From 3e16faddb1b65e97aae00fb4829b3edbfafd96db Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 6 Apr 2023 11:38:25 -0700 Subject: [PATCH 43/49] Fix formatting and add test "reminders" --- src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs | 2 +- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index 76f61c50ecd45..64f0e1c48553f 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -248,7 +248,7 @@ public Location? InterceptableLocation { return null; } - + // If a qualified name is used as a valid receiver of an invocation syntax at some point, // we probably want to treat it similarly to a MemberAccessExpression. // However, we don't expect to encounter it. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 88ea94aaed7ef..cae95bbc184b6 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -198,6 +198,8 @@ static class D // * An explicit interface implementation marked as interceptable // * Intercept a generic method call when the type parameters are / are not substituted + // PROTOTYPE(ic): test a valid case with large numbers of interceptors. (EndToEndTests?) + [Fact] public void InterceptableInstanceMethod_InterceptorExtensionMethod() { @@ -1131,6 +1133,8 @@ static class D ); } + // PROTOTYPE(ic): test when parameter names differ or appear in a different order in interceptor vs interceptable method. + [Fact] public void InterpolatedStringHandler_01() { From 2ca2c93b92cec31ad970977c7ac43021f0494561 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 11 Apr 2023 11:12:25 -0700 Subject: [PATCH 44/49] Update docs/features/interceptors.md Co-authored-by: Julien Couvreur --- docs/features/interceptors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 8abbdc0e6d1f4..f24b6bbecb057 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -155,7 +155,7 @@ There's a concern on the ASP.NET side that if the user needs to opt-in to someth ### Implementation strategy During the binding phase, `InterceptsLocationAttribute` usages are decoded and the related data for each usage are collected in a `ConcurrentSet` on the compilation: -- attribute arguments +- intercepted file-path and location - attribute location - attributed method symbol PROTOTYPE(ic): the exact collection used to collect the attribute usages, and the exact way it is used, are not finalized. The main concern is to ensure we can scale to large numbers of interceptors without issue, and that we can report diagnostics for duplicate interception of the same location in a deterministic way. From 76e36daac62aba6e6ac100254bbcfa588f83960b Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 11 Apr 2023 14:22:50 -0700 Subject: [PATCH 45/49] Address feedback --- docs/features/interceptors.md | 9 +- .../Portable/BoundTree/BoundExpression.cs | 2 +- .../CSharp/Portable/CSharpResources.resx | 13 +- .../Portable/Compilation/CSharpCompilation.cs | 10 +- .../CSharp/Portable/Errors/ErrorCode.cs | 3 + .../LocalRewriter/LocalRewriter_Call.cs | 10 +- .../CSharp/Portable/Symbols/MethodSymbol.cs | 3 + .../SourceMethodSymbolWithAttributes.cs | 37 +- .../Portable/xlf/CSharpResources.cs.xlf | 23 +- .../Portable/xlf/CSharpResources.de.xlf | 23 +- .../Portable/xlf/CSharpResources.es.xlf | 23 +- .../Portable/xlf/CSharpResources.fr.xlf | 23 +- .../Portable/xlf/CSharpResources.it.xlf | 23 +- .../Portable/xlf/CSharpResources.ja.xlf | 23 +- .../Portable/xlf/CSharpResources.ko.xlf | 23 +- .../Portable/xlf/CSharpResources.pl.xlf | 23 +- .../Portable/xlf/CSharpResources.pt-BR.xlf | 23 +- .../Portable/xlf/CSharpResources.ru.xlf | 23 +- .../Portable/xlf/CSharpResources.tr.xlf | 23 +- .../Portable/xlf/CSharpResources.zh-Hans.xlf | 23 +- .../Portable/xlf/CSharpResources.zh-Hant.xlf | 23 +- .../Semantic/Semantics/InterceptorsTests.cs | 327 ++++++++++++++++-- 22 files changed, 606 insertions(+), 107 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index f24b6bbecb057..7a1427a581f26 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -14,6 +14,7 @@ using System.Runtime.CompilerServices; var c = new C(); c.InterceptableMethod(1); // (L1,C1): prints "interceptor 1" c.InterceptableMethod(1); // (L2,C2): prints "other interceptor 1" +c.InterceptableMethod(2); // (L3,C3): prints "other interceptor 2" c.InterceptableMethod(1); // prints "interceptable 1" class C @@ -35,6 +36,7 @@ static class D } [InterceptsLocation("Program.cs", line: /*L2*/, character: /*C2*/)] // refers to the call at (L2, C2) + [InterceptsLocation("Program.cs", line: /*L3*/, character: /*C3*/)] // refers to the call at (L3, C3) public static void OtherInterceptorMethod(this C c, int param) { Console.WriteLine($"other interceptor {param}"); @@ -115,12 +117,17 @@ Interceptors cannot have type parameters or be declared in generic types at any ### Signature matching +PROTOTYPE(ic): It is suggested to permit nullability differences and other comparable differences. Perhaps we can revisit the matching requirements of "partial methods" and imitate them here. + When a call is intercepted, the interceptor and interceptable methods must meet the signature matching requirements detailed below: - When an interceptable instance method is compared to a classic extension method, we use the extension method in reduced form for comparison. The extension method parameter with the `this` modifier is compared to the instance method `this` parameter. - The returns and parameters, including the `this` parameter, must have the same ref kinds and types, except that reference types with oblivious nullability can match either annotated or unannotated reference types. - Method names and parameter names are not required to match. - Parameter default values are not required to match. When intercepting, default values on the interceptor method are ignored. -- `scoped` modifiers and `[UnscopedRefAttribute]` must be equivalent. +- `params` modifiers are not required to match. +- `scoped` modifiers and `[UnscopedRef]` must be equivalent. +- In general, attributes which normally affect the behavior of the call site, such as `[CallerLineNumber]` are ignored on the interceptor of an intercepted call. + - The only exception to this is when the attribute affects "capabilities" of the method in a way that affects safety, such as with `[UnscopedRef]`. In this case, attributes are required to match across interceptable and interceptor methods. Arity does not need to match between intercepted and interceptor methods. In other words, it is permitted to intercept a generic method with a non-generic interceptor. diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index 64f0e1c48553f..54a3d553ba574 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -5,9 +5,9 @@ using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Roslyn.Utilities; using System; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.CodeAnalysis.CSharp { diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 11f8c36c04773..3a75572471000 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7506,10 +7506,10 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ A switch expression arm does not begin with a 'case' keyword. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -7547,4 +7547,13 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Interceptor must not have a 'this' parameter because '{0}' does not have a 'this' parameter. + + Interceptor cannot have a 'null' file path. + + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + \ 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 f0b187884c21e..1e6ecbbc62847 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -2262,10 +2262,12 @@ internal void AddInterception(InterceptsLocationAttributeData location, MethodSy return null; } + var sourceTree = callLocation.SourceTree; + Debug.Assert(sourceTree is not null); + var callLineColumn = callLocation.GetLineSpan().Span.Start; foreach (var (interceptsLocation, interceptor) in _interceptions) { - var callLineColumn = callLocation.GetLineSpan().Span.Start; - if (interceptsLocation.FilePath == callLocation.SourceTree!.FilePath + if (interceptsLocation.FilePath == sourceTree.FilePath && interceptsLocation.Line == callLineColumn.Line && interceptsLocation.Character == callLineColumn.Character) { @@ -2276,7 +2278,7 @@ internal void AddInterception(InterceptsLocationAttributeData location, MethodSy return null; } - private void BuildInterceptorsMap() + private void BuildInterceptionsMap() { // PROTOTYPE(ic): build a map where we can quickly lookup with a location and get a symbol. // At this time, should report any duplicate interception diagnostics. @@ -3273,7 +3275,7 @@ internal override bool CompileMethods( return false; } - BuildInterceptorsMap(); + BuildInterceptionsMap(); // Perform initial bind of method bodies in spite of earlier errors. This is the same // behavior as when calling GetDiagnostics() diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index b9f21aa6deae7..a60815d88e54f 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2204,6 +2204,9 @@ internal enum ErrorCode ERR_InterceptorMustReferToStartOfTokenPosition = 27010, ERR_InterceptorMustHaveMatchingThisParameter = 27011, ERR_InterceptorMustNotHaveThisParameter = 27012, + ERR_InterceptorFilePathCannotBeNull = 27013, + ERR_InterceptorNameNotInvoked = 27014, + ERR_InterceptorNonUniquePath = 27015, #endregion // Note: you will need to do the following after adding warnings: diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 4d1aa43c51756..092b989bdbf37 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -133,7 +133,7 @@ private void EmbedIfNeedTo(BoundExpression receiver, ImmutableArray arguments, @@ -145,6 +145,7 @@ public 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. + // PROTOTYPE: perhaps a 'TryGet' pattern is more suitable here. if (this._compilation.GetInterceptor(interceptableLocation) is not var (interceptsLocationAttributeData, interceptor)) { // The call was not intercepted. @@ -191,7 +192,6 @@ public void InterceptCallAndAdjustArguments( break; default: break; - } if (needToReduce) @@ -226,20 +226,18 @@ public override BoundNode VisitCall(BoundCall node) Debug.Assert(node != null); MethodSymbol method = node.Method; - BoundExpression? receiverOpt = node.ReceiverOpt; - ImmutableArray arguments = node.Arguments; ImmutableArray argsToParamsOpt = node.ArgsToParamsOpt; ImmutableArray argRefKindsOpt = node.ArgumentRefKindsOpt; bool invokedAsExtensionMethod = node.InvokedAsExtensionMethod; // Rewrite the receiver - BoundExpression? rewrittenReceiver = VisitExpression(receiverOpt); + BoundExpression? rewrittenReceiver = VisitExpression(node.ReceiverOpt); ArrayBuilder? temps = null; var rewrittenArguments = VisitArgumentsAndCaptureReceiverIfNeeded( ref rewrittenReceiver, captureReceiverMode: ReceiverCaptureMode.Default, - arguments, + node.Arguments, method, argsToParamsOpt, argRefKindsOpt, diff --git a/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs index f8a9584b6cfff..d8fd84a9cc348 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs @@ -94,6 +94,9 @@ public virtual bool IsGenericMethod /// internal virtual bool IsDirectlyExcludedFromCodeCoverage { get => false; } + /// + /// True if the method is annotated with the `[Interceptable]` attribute. + /// internal abstract bool IsInterceptable { get; } /// diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 650da4c662c87..af56d2656cbb2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -948,7 +948,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments Debug.Assert(!arguments.Attribute.HasErrors); var attributeArguments = arguments.Attribute.CommonConstructorArguments; if (attributeArguments is not [ - { Kind: not TypedConstantKind.Array, Value: string filePath }, + { Type.SpecialType: SpecialType.System_String }, { Kind: not TypedConstantKind.Array, Value: int displayLineNumber }, { Kind: not TypedConstantKind.Array, Value: int displayCharacterNumber }]) { @@ -956,14 +956,16 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments throw ExceptionUtilities.Unreachable(); } - // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. - int lineNumber = displayLineNumber - 1; - int characterNumber = displayCharacterNumber - 1; - - // PROTOTYPE(ic): test with zero or negative display line/character numbers. - var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; var attributeLocation = arguments.AttributeSyntaxOpt.Location; + + var filePath = (string?)attributeArguments[0].Value; + if (filePath is null) + { + diagnostics.Add(ErrorCode.ERR_InterceptorFilePathCannotBeNull, attributeLocation); + return; + } + if (Arity != 0 || ContainingType.IsGenericType) { // PROTOTYPE(ic): for now, let's disallow type arguments on the method or containing types. @@ -981,8 +983,8 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments } var syntaxTrees = DeclaringCompilation.SyntaxTrees; - var matchingTree = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath == filePath, filePath); - if (matchingTree == null) + var matchingTrees = syntaxTrees.WhereAsArray(static (tree, filePath) => tree.FilePath == filePath, filePath); + if (matchingTrees is []) { var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath.EndsWith(filePath), filePath); if (suffixMatch != null) @@ -997,6 +999,17 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } + if (matchingTrees is not [var matchingTree]) + { + diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeLocation, filePath); + return; + } + + // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. + // PROTOTYPE(ic): test with zero or negative display line/character numbers. + int lineNumber = displayLineNumber - 1; + int characterNumber = displayCharacterNumber - 1; + var referencedLines = matchingTree.GetText().Lines; var referencedLineCount = referencedLines.Count; if (lineNumber >= referencedLineCount) @@ -1022,6 +1035,12 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments case { Parent: SimpleNameSyntax { Parent: InvocationExpressionSyntax invocation } simpleName } when invocation.Expression == simpleName: // happy case break; + case { Parent: SimpleNameSyntax { Parent: not MemberAccessExpressionSyntax } }: + case { Parent: SimpleNameSyntax { Parent: MemberAccessExpressionSyntax memberAccess } rhs } when memberAccess.Name == rhs: + // NB: there are all sorts of places "simple names" can appear in syntax. With these checks we are trying to + // minimize confusion about why the name being used is not *interceptable*, but it's done on a best-effort basis. + diagnostics.Add(ErrorCode.ERR_InterceptorNameNotInvoked, attributeLocation, referencedToken.Text); + return; default: diagnostics.Add(ErrorCode.ERR_InterceptorPositionBadToken, attributeLocation, referencedToken.Text); return; diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 629b77cd67b27..c4fbff9f19d79 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 1ab4f238db080..f37e7913a93fe 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index d0b36e6614a50..a84cdfdd07322 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index c2fc03c44660f..0c5a4ff6cd72d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 118b3d28b8ef3..c21849632120d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index ef1b31e0a01c7..7c173929e51b8 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index 53fab1103a6c0..381ee2863697c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index b7e205b4d0958..9f1b32cdeb8b7 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index e9974d3de47be..c2f8a46f01a73 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index a825cb95ad61d..e6572a2510fb8 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index e45dfa554297d..dce2f22aa7b91 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index ab1ea558bef6f..83178c6eaa8ef 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index ce6c2f2f7e4a1..a2ee19f7133dc 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -852,6 +852,11 @@ The given line is '{0}' characters long, which is fewer than the provided character number '{1}'. + + Interceptor cannot have a 'null' file path. + Interceptor cannot have a 'null' file path. + + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -877,6 +882,16 @@ The provided character number does not refer to the start of method name token '{0}'. Consider using character number '{1}' instead. + + Possible method name '{0}' cannot be intercepted because it is not being invoked. + Possible method name '{0}' cannot be intercepted because it is not being invoked. + + + + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + Cannot intercept a call in file with path '{0}' because multiple files in the compilation have this path. + + Cannot intercept: compilation does not contain a file with path '{0}'. Cannot intercept: compilation does not contain a file with path '{0}'. @@ -2133,13 +2148,13 @@ - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to '{0}' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to '{0}' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. - Cannot intercept a call to a method which is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + Call to a method is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index cae95bbc184b6..9d861da873227 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -12,6 +12,35 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; public class InterceptorsTests : CSharpTestBase { + // PROTOTYPE(ic): test a case where the original method has type parameter constraints. + // PROTOTYPE(ic): for now we will just completely disallow type parameters in the interceptor. + // PROTOTYPE(ic): test where interceptable is a constructed method or a retargeting method. + // PROTOTYPE(ic): test where there are differences between 'scoped' modifiers and '[UnscopedRef]' attributes + + // PROTOTYPE(ic): Possible test cases: + // + // * Intercept instance method with instance method in same class, base class, derived class + // * Intercept with extern method + // * Intercept with abstract or interface 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 + // * Intercept a generic method call when the type parameters are / are not substituted + + // PROTOTYPE(ic): test a valid case with large numbers of interceptors. (EndToEndTests?) + // 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. + + // PROTOTYPE(ic): test when parameter names differ or appear in a different order in interceptor vs interceptable method. + + // PROTOTYPE(ic): duplicates + // PROTOTYPE(ic): intercept with instance method + // PROTOTYPE(ic): intercept with instance base method + + // PROTOTYPE(ic): test interceptable explicit interface implementation (should error). + // PROTOTYPE(ic): test interceptor with 'ref this' to match a struct interceptable method. + private static readonly (string, string) s_attributesSource = (""" namespace System.Runtime.CompilerServices; @@ -181,25 +210,6 @@ static class D verifier.VerifyDiagnostics(); } - // PROTOTYPE(ic): test a case where the original method has type parameter constraints. - // PROTOTYPE(ic): for now we will just completely disallow type parameters in the interceptor. - // PROTOTYPE(ic): test where interceptable is a constructed method or a retargeting method. - // PROTOTYPE(ic): test where there are differences between 'scoped' modifiers and '[UnscopedRef]' attributes - - // PROTOTYPE(ic): Possible test cases: - // - // * Intercept instance method with instance method in same class, base class, derived class - // * Intercept with extern method - // * Intercept with abstract or interface 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 - // * Intercept a generic method call when the type parameters are / are not substituted - - // PROTOTYPE(ic): test a valid case with large numbers of interceptors. (EndToEndTests?) - [Fact] public void InterceptableInstanceMethod_InterceptorExtensionMethod() { @@ -232,6 +242,236 @@ static class D verifier.VerifyDiagnostics(); } + [Fact] + public void InterceptableInstanceMethod_InterceptorStaticMethod() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public C InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 11)] + public static C Interceptor1(C i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(15,11): error CS27007: Cannot intercept method 'C.InterceptableMethod(string)' with interceptor 'D.Interceptor1(C, string)' because the signatures do not match. + // c.InterceptableMethod("call site"); + Diagnostic(ErrorCode.ERR_InterceptorSignatureMismatch, "InterceptableMethod").WithArguments("C.InterceptableMethod(string)", "D.Interceptor1(C, string)").WithLocation(15, 11)); + } + + [Fact] + public void InterceptsLocationDuplicatePath() + { + var source0 = (""" + public class D0 + { + public static void M() + { + C.InterceptableMethod("a"); + } + } + """, "Program.cs"); + + var source1 = (""" + public class D1 + { + public static void M() + { + C.InterceptableMethod("a"); + } + } + """, "Program.cs"); + + var source2 = (""" + using System.Runtime.CompilerServices; + using System; + + D0.M(); + D1.M(); + + public class C + { + [Interceptable] + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + } + + public static class Interceptor + { + [InterceptsLocation("Program.cs", 5, 11)] + public static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + """, "Interceptor.cs"); + + var comp = CreateCompilation(new[] { source0, source1, source2, s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Interceptor.cs(15,6): error CS27015: Cannot intercept a call in file with path 'Program.cs' because multiple files in the compilation have this path. + // [InterceptsLocation("Program.cs", 5, 11)] + Diagnostic(ErrorCode.ERR_InterceptorNonUniquePath, @"InterceptsLocation(""Program.cs"", 5, 11)").WithArguments("Program.cs").WithLocation(15, 6)); + } + + [Fact] + public void InterceptsLocationFromMetadata() + { + // Verify that `[InterceptsLocation]` on a method from metadata does not cause a call in the current compilation to be intercepted. + var source0 = """ + using System.Runtime.CompilerServices; + using System; + + public class C0 + { + [Interceptable] + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + + static void M0() + { + InterceptableMethod("1"); + } + } + + public static class D + { + [InterceptsLocation("Program.cs", 11, 9)] + public static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + """; + var comp0 = CreateCompilation(new[] { (source0, "Program.cs"), s_attributesSource }); + comp0.VerifyEmitDiagnostics(); + + var source1 = """ + using System.Runtime.CompilerServices; + using System; + + class C1 + { + [Interceptable] + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + + static void Main() + { + InterceptableMethod("1"); + } + } + """; + + var comp1 = CompileAndVerify(new[] { (source1, "Program.cs") }, new[] { comp0.ToMetadataReference() }, expectedOutput: "interceptable 1"); + comp1.VerifyDiagnostics(); + } + + [Fact] + public void InterceptableDelegateConversion() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + [Interceptable] + public C InterceptableMethod(string param) { Console.Write("interceptable " + param); return this; } + } + + static class Program + { + public static void Main() + { + var c = new C(); + var del = c.InterceptableMethod; + } + } + + static class D + { + [InterceptsLocation("Program.cs", 15, 21)] + public static C Interceptor1(this C i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """; + var compilation = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + compilation.VerifyEmitDiagnostics( + // Program.cs(21,6): error CS27014: Possible method name 'InterceptableMethod' cannot be intercepted because it is not being invoked. + // [InterceptsLocation("Program.cs", 15, 21)] + Diagnostic(ErrorCode.ERR_InterceptorNameNotInvoked, @"InterceptsLocation(""Program.cs"", 15, 21)").WithArguments("InterceptableMethod").WithLocation(21, 6) + ); + } + + [Fact] + public void InterceptableNameof() + { + var source = """ + using System.Runtime.CompilerServices; + + static class Program + { + public static void Main() + { + _ = nameof(Main); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 7, 13)] + 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( + ); + } + + [Fact] + public void InterceptableDelegateInvocation() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + C.M(() => Console.Write(0)); + + static class C + { + public static void M(Action action) + { + action(); + } + } + + static class D + { + [InterceptsLocation("Program.cs", 10, 9)] + public static void Interceptor1(this Action action) { Console.Write(1); } + } + """; + 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. + 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)] + Diagnostic(ErrorCode.WRN_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 10, 9)").WithArguments("System.Action.Invoke()").WithLocation(16, 6) + ); + } + [Fact] public void QualifiedNameAtCallSite() { @@ -327,9 +567,6 @@ class C Diagnostic(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, @"InterceptsLocation(""Program.cs"", 8, 11)").WithArguments("C.InterceptableMethod(string)").WithLocation(17, 6)); } - // 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. - - // PROTOTYPE(ic): test when parameter names differ between interceptable and interceptor. [Fact] public void ArgumentLabels() { @@ -471,10 +708,6 @@ public static void Main() ); } - // PROTOTYPE(ic): duplicates - // PROTOTYPE(ic): intercept with instance method - // PROTOTYPE(ic): intercept with instance base method - [Fact] public void LocalFunctionInterceptable() { @@ -535,7 +768,7 @@ static class D """; var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, expectedOutput: "interceptor call site"); verifier.VerifyDiagnostics( - // Program.cs(20,6): warning CS27000: Cannot intercept a call to 'C.InterceptableMethod(string)' because it is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. + // Program.cs(20,6): warning CS27000: Call to 'C.InterceptableMethod(string)' is intercepted, but the method is not marked with 'System.Runtime.CompilerServices.InterceptableAttribute'. // [InterceptsLocation("Program.cs", 14, 11)] Diagnostic(ErrorCode.WRN_CallNotInterceptable, @"InterceptsLocation(""Program.cs"", 14, 11)").WithArguments("C.InterceptableMethod(string)").WithLocation(20, 6) ); @@ -832,6 +1065,41 @@ static class D ); } + [Fact] + public void InterceptsLocationBadPath_03() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C { } + + static class Program + { + [Interceptable] + public static C InterceptableMethod(this C c, string param) { Console.Write("interceptable " + param); return c; } + + public static void Main() + { + var c = new C(); + c.InterceptableMethod("call site"); + } + } + + static class D + { + [InterceptsLocation(null, 15, 11)] + public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(20,6): error CS27013: Interceptor cannot have a 'null' file path. + // [InterceptsLocation(null, 15, 11)] + Diagnostic(ErrorCode.ERR_InterceptorFilePathCannotBeNull, "InterceptsLocation(null, 15, 11)").WithLocation(20, 6) + ); + } + [Fact] public void InterceptsLocationBadPosition_01() { @@ -1093,9 +1361,6 @@ static class D ); } - // PROTOTYPE(ic): test interceptable explicit interface implementation (should error). - // PROTOTYPE(ic): test interceptor with 'ref this' to match a struct interceptable method. - [Fact] public void SignatureMismatch_03() { @@ -1133,8 +1398,6 @@ static class D ); } - // PROTOTYPE(ic): test when parameter names differ or appear in a different order in interceptor vs interceptable method. - [Fact] public void InterpolatedStringHandler_01() { From 692ba43dd6a4b2bcbe0039e253df8516c8a81615 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 11 Apr 2023 22:35:32 -0700 Subject: [PATCH 46/49] Update spec. Fix IsBuildOnlyDiagnostic. --- docs/features/interceptors.md | 36 ++++++++----------- .../CSharp/Portable/Errors/ErrorFacts.cs | 3 ++ .../Semantic/Semantics/InterceptorsTests.cs | 1 - 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 7a1427a581f26..42557fe9e9964 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -3,7 +3,7 @@ ## Summary [summary]: #summary -*Interceptors* are an experimental compiler feature. +*Interceptors* are an experimental compiler feature planned to ship in .NET 8. The feature may be subject to breaking changes or removal in a future release. An *interceptor* is a method which can declaratively substitute a call to an *interceptable* method with a call to itself at compile time. This substitution occurs by having the interceptor declare the source locations of the calls that it intercepts. This provides a limited facility to change the semantics of existing code by adding new code to a compilation (e.g. in a source generator). @@ -49,9 +49,9 @@ static class D ### InterceptableAttribute -A method must indicate that its calls can be *intercepted* by including `[Interceptable]` on its declaration. +A method can indicate that its calls can be *intercepted* by including `[Interceptable]` on its declaration. -If a call is intercepted to a method which lacks this attribute, a warning is reported, and interception still occurs. This may be changed to an error in the future. +PROTOTYPE(ic): For now, if a call is intercepted to a method which lacks this attribute, a warning is reported, and interception still occurs. This may be changed to an error in the future. ```cs namespace System.Runtime.CompilerServices @@ -75,17 +75,9 @@ namespace System.Runtime.CompilerServices } ``` -PROTOTYPE(ic): open question in https://github.com/dotnet/roslyn/pull/67432#discussion_r1147822738 +`[InterceptsLocation]` attributes included in source are emitted to the resulting assembly, just like other custom attributes. -Are the `[InterceptsLocation]` attributes dropped when emitting the methods to metadata? - -No. We could consider: - -* having the compiler drop the attributes automatically (treating them as pseudo-custom attributes, perhaps) -* making the attribute declaration in the framework "conditional" on some debug symbol which is not usually provided. -* deliberately keeping them there. I can't think of any good reason to do this. - -Perhaps the first option would be best. +PROTOTYPE(ic): We may want to recognize `file class InterceptsLocationAttribute` as a valid declaration of the attribute, to allow generators to bring the attribute in without conflicting with other generators which may also be bringing the attribute in. See open question in [User opt-in](#user-opt-in). #### File paths @@ -93,6 +85,8 @@ File paths used in `[InterceptsLocation]` must exactly match the paths on the sy The compiler does not map `#line` directives when determining if an `[InterceptsLocation]` attribute intercepts a particular call in syntax. +PROTOTYPE(ic): editorconfig support matches paths in cross-platform fashion (e.g. normalizing slashes). We should revisit how that works and consider if the same matching strategy should be used instead of ordinal comparison. + #### Position Line and column numbers in `[InterceptsLocation]` are 1-indexed to match existing places where source locations are displayed to the user. For example, in `Diagnostic.ToString`. @@ -109,7 +103,7 @@ We should provide samples of recommended coding patterns for generator authors t Conversion to delegate type, address-of, etc. usages of methods cannot be intercepted. -Interception can only occur for calls to ordinary member methods--not constructors, delegates, properties, local functions, etc. +Interception can only occur for calls to ordinary member methods--not constructors, delegates, properties, local functions, operators, etc. Support for more member kinds may be added in the future. ### Arity @@ -141,9 +135,11 @@ If an `[InterceptsLocation]` attribute is found in the compilation which does no An interceptor must be accessible at the location where interception is occurring. PROTOTYPE(ic): This enforcement is not yet implemented. -We imagine it will be common to want to discourage explicit use of interceptor methods. For this use case, should consider adjusting behavior of `[EditorBrowsable]` to work in the same compilation, and encouraging generator authors to use it to prevent interceptors from appearing in lookup, etc. +An interceptor contained in a file-local type is permitted to intercept a call in another file, even though the interceptor is not normally *visible* at the call site. -PROTOTYPE(ic): Generators often want to put things not intended to be user-visible in file-local types. This reduces the need to defend against name conflicts with other types in the user's project. A file-local type is not present in lookup outside of the file it is declared in. But, the type is *accessible* from the runtime point of view presently. Should we permit interceptors declared in file types to refer to other files? +This allows generator authors to avoid *polluting lookup* with interceptors, helps avoid name conflicts, and prevents use of interceptors in *unintended positions* from the interceptor author's point-of-view. + +We may also want to consider adjusting behavior of `[EditorBrowsable]` to work in the same compilation. ### Editor experience @@ -151,13 +147,9 @@ Interceptors are treated like a post-compilation step in this design. Diagnostic ### User opt-in -PROTOTYPE(ic): design an opt-in step which meets the needs of the generators consuming this, and permits usage of the feature in the key NativeAOT scenarios we are targeting. - -Because this is a generator-oriented feature, we'd like to have an opt-in step during the *experimental* phase which permits generators to opt-in without the end user needing to take additional steps. - -Question: can a generator opt-in to a preview feature on behalf of the user? If not, is it fine to ask the user to take an additional step here? +Although interceptors are an experimental feature, there will be no explicit opt-in step needed to use them. We won't publicize the feature (e.g. in blog posts) as something generator authors should onboard to in .NET 8. -There's a concern on the ASP.NET side that if the user needs to opt-in to something which is "preview/experimental" then they will need to use their non-interceptors codegen strategy *as well as* the interceptors one for NativeAOT, depending on whether the user did the gesture to enable the feature. +PROTOTYPE(ic): The BCL might not ship the attributes required by this feature, instead requiring them to be declared in some library brought in by a package reference, or in the user's project. But we haven't confirmed this. ### Implementation strategy diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 8dc9015d92277..b8e3a06dd5ab0 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -2323,6 +2323,9 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_InterceptableMethodMustBeOrdinary: case ErrorCode.ERR_InterceptorMethodMustBeOrdinary: case ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition: + case ErrorCode.ERR_InterceptorFilePathCannotBeNull: + case ErrorCode.ERR_InterceptorNameNotInvoked: + case ErrorCode.ERR_InterceptorNonUniquePath: return false; default: // NOTE: All error codes must be explicitly handled in this switch statement diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 9d861da873227..27ab320becf02 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -173,7 +173,6 @@ file class D } """; - // PROTOTYPE(ic): Consider if an error should be reported here. It's not possible to directly use 'Interceptor1' at the location of the intercepted call. var verifier = CompileAndVerify(new[] { (source1, "Program.cs"), (source2, "Other.cs"), s_attributesSource }, expectedOutput: "interceptor 1"); verifier.VerifyDiagnostics(); } From 6359a1d77d9220939b6fe860f76b4bc7812c5cf9 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 12 Apr 2023 11:16:00 -0700 Subject: [PATCH 47/49] Address feedback --- .../Semantic/Semantics/InterceptorsTests.cs | 102 ++++++++++++++---- 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 27ab320becf02..e0c564f56e8d5 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -673,37 +673,45 @@ static class D } [Fact] - public void LocalFunctionInterceptor() + public void BadMethodKind() { var source = """ using System.Runtime.CompilerServices; - using System; - - interface I1 { } - class C : I1 { } static class Program { [Interceptable] - public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + public static void InterceptableMethod(string param) { } public static void Main() { - InterceptableMethod("call site"); + InterceptableMethod(""); + Interceptor1(""); + + var lambda = [InterceptsLocation("Program.cs", 13, 8)] (string param) => { }; // 1 - [InterceptsLocation("Program.cs", 13, 8)] - static void Interceptor1(string param) { Console.Write("interceptor " + param); } + [InterceptsLocation("Program.cs", 13, 8)] // 2 + static void Interceptor1(string param) { } + } + + public static string Prop + { + [InterceptsLocation("Program.cs", 13, 8)] // 3 + set { } } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyDiagnostics( - // Program.cs(16,10): error CS27009: An interceptor method must be an ordinary member method. - // [InterceptsLocation("Program.cs", 13, 8)] - Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, @"InterceptsLocation(""Program.cs"", 13, 8)").WithLocation(16, 10), - // Program.cs(17,21): warning CS8321: The local function 'Interceptor1' is declared but never used - // static void Interceptor1(string param) { Console.Write("interceptor " + param); } - Diagnostic(ErrorCode.WRN_UnreferencedLocalFunction, "Interceptor1").WithArguments("Interceptor1").WithLocation(17, 21) + // Program.cs(13,23): error CS27009: An interceptor method must be an ordinary member method. + // var lambda = [InterceptsLocation("Program.cs", 13, 8)] (string param) => { }; // 1 + Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, @"InterceptsLocation(""Program.cs"", 13, 8)").WithLocation(13, 23), + // Program.cs(15,10): error CS27009: An interceptor method must be an ordinary member method. + // [InterceptsLocation("Program.cs", 13, 8)] // 2 + Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, @"InterceptsLocation(""Program.cs"", 13, 8)").WithLocation(15, 10), + // Program.cs(21,10): error CS27009: An interceptor method must be an ordinary member method. + // [InterceptsLocation("Program.cs", 13, 8)] // 3 + Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, @"InterceptsLocation(""Program.cs"", 13, 8)").WithLocation(21, 10) ); } @@ -1123,15 +1131,23 @@ public static void Main() static class D { + [InterceptsLocation("Program.cs", 25, 1)] + [InterceptsLocation("Program.cs", 26, 1)] [InterceptsLocation("Program.cs", 100, 1)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(21,6): error CS27005: The given file has '23' lines, which is fewer than the provided line number '100'. - // [InterceptsLocation("Program.cs", 100, 1)] - Diagnostic(ErrorCode.ERR_InterceptorLineOutOfRange, @"InterceptsLocation(""Program.cs"", 100, 1)").WithArguments("23", "100").WithLocation(21, 6) + // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '}'. + // [InterceptsLocation("Program.cs", 25, 1)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 25, 1)").WithArguments("}").WithLocation(21, 6), + // Program.cs(22,6): error CS27005: The given file has '25' lines, which is fewer than the provided line number '26'. + // [InterceptsLocation("Program.cs", 26, 1)] + Diagnostic(ErrorCode.ERR_InterceptorLineOutOfRange, @"InterceptsLocation(""Program.cs"", 26, 1)").WithArguments("25", "26").WithLocation(22, 6), + // Program.cs(23,6): error CS27005: The given file has '25' lines, which is fewer than the provided line number '100'. + // [InterceptsLocation("Program.cs", 100, 1)] + Diagnostic(ErrorCode.ERR_InterceptorLineOutOfRange, @"InterceptsLocation(""Program.cs"", 100, 1)").WithArguments("25", "100").WithLocation(23, 6) ); } @@ -1159,15 +1175,23 @@ public static void Main() static class D { + [InterceptsLocation("Program.cs", 16, 5)] + [InterceptsLocation("Program.cs", 16, 6)] [InterceptsLocation("Program.cs", 16, 1000)] public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); comp.VerifyEmitDiagnostics( - // Program.cs(21,6): error CS27006: The given line is '5' characters long, which is fewer than the provided character number '1000'. - // [InterceptsLocation("Program.cs", 15, 1000)] - Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 16, 1000)").WithArguments("5", "1000").WithLocation(21, 6) + // Program.cs(21,6): error CS27004: The provided line and character number does not refer to an interceptable method name, but rather to token '}'. + // [InterceptsLocation("Program.cs", 16, 5)] + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, @"InterceptsLocation(""Program.cs"", 16, 5)").WithArguments("}").WithLocation(21, 6), + // Program.cs(22,6): error CS27006: The given line is '5' characters long, which is fewer than the provided character number '6'. + // [InterceptsLocation("Program.cs", 16, 6)] + Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 16, 6)").WithArguments("5", "6").WithLocation(22, 6), + // Program.cs(23,6): error CS27006: The given line is '5' characters long, which is fewer than the provided character number '1000'. + // [InterceptsLocation("Program.cs", 16, 1000)] + Diagnostic(ErrorCode.ERR_InterceptorCharacterOutOfRange, @"InterceptsLocation(""Program.cs"", 16, 1000)").WithArguments("5", "1000").WithLocation(23, 6) ); } @@ -1286,6 +1310,42 @@ static class CExt ); } + [Fact] + public void InterceptsLocationBadPosition_06() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C { } + + static class Program + { + public static void Main() + { + var c = new C(); + c.InterceptableMethod/*comment*/("call site"); + } + + [Interceptable] + public static C InterceptableMethod(this C c, string param) { Console.Write("interceptable " + param); return c; } + + [InterceptsLocation("Program.cs", 11, 31)] // intercept comment after 'InterceptableMethod' token + public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } + } + + static class CExt + { + } + """; + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(17,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '11' instead. + // [InterceptsLocation("Program.cs", 11, 31)] // intercept comment after 'InterceptableMethod' token + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 11, 31)").WithArguments("InterceptableMethod", "11").WithLocation(17, 6) + ); + } + [Fact] public void SignatureMismatch_01() { From 3206c7fa63418ecc1af75c39bfdb0b6418d7c1fd Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 12 Apr 2023 11:50:33 -0700 Subject: [PATCH 48/49] Use zero/one based naming convention. Add tests etc. --- .../SourceMethodSymbolWithAttributes.cs | 26 ++++++------ .../Semantic/Semantics/InterceptorsTests.cs | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index af56d2656cbb2..1c9b963f7ed06 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -949,8 +949,8 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments var attributeArguments = arguments.Attribute.CommonConstructorArguments; if (attributeArguments is not [ { Type.SpecialType: SpecialType.System_String }, - { Kind: not TypedConstantKind.Array, Value: int displayLineNumber }, - { Kind: not TypedConstantKind.Array, Value: int displayCharacterNumber }]) + { Kind: not TypedConstantKind.Array, Value: int lineNumberOneBased }, + { Kind: not TypedConstantKind.Array, Value: int characterNumberOneBased }]) { // Since the attribute does not have errors (asserted above), it should be guaranteed that we have the above arguments. throw ExceptionUtilities.Unreachable(); @@ -1007,26 +1007,26 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. // PROTOTYPE(ic): test with zero or negative display line/character numbers. - int lineNumber = displayLineNumber - 1; - int characterNumber = displayCharacterNumber - 1; + int lineNumberZeroBased = lineNumberOneBased - 1; + int characterNumberZeroBased = characterNumberOneBased - 1; var referencedLines = matchingTree.GetText().Lines; var referencedLineCount = referencedLines.Count; - if (lineNumber >= referencedLineCount) + if (lineNumberZeroBased >= referencedLineCount) { - diagnostics.Add(ErrorCode.ERR_InterceptorLineOutOfRange, attributeLocation, referencedLineCount, displayLineNumber); + diagnostics.Add(ErrorCode.ERR_InterceptorLineOutOfRange, attributeLocation, referencedLineCount, lineNumberOneBased); return; } - var line = referencedLines[lineNumber]; + var line = referencedLines[lineNumberZeroBased]; var lineLength = line.End - line.Start; - if (characterNumber >= lineLength) + if (characterNumberZeroBased >= lineLength) { - diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeLocation, lineLength, displayCharacterNumber); + diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeLocation, lineLength, characterNumberOneBased); return; } - var referencedPosition = line.Start + characterNumber; + var referencedPosition = line.Start + characterNumberZeroBased; var root = matchingTree.GetRoot(); var referencedToken = root.FindToken(referencedPosition); switch (referencedToken) @@ -1050,15 +1050,15 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments var tokenPositionDifference = referencedPosition - referencedToken.Span.Start; if (tokenPositionDifference != 0) { - // tokens don't span multiple lines, so we can apply the difference we found to the characterNumber within the line and figure out which character the user should have used. - diagnostics.Add(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, attributeLocation, referencedToken.Text, displayCharacterNumber - tokenPositionDifference); + // PROTOTYPE(ic): when a token's leading trivia spans multiple lines, this doesn't suggest a valid position. + diagnostics.Add(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, attributeLocation, referencedToken.Text, characterNumberOneBased - tokenPositionDifference); return; } // PROTOTYPE(ic): The attribute should probably be expected to contain "display locations" (1-indexed) a la Diagnostic.ToString(). // But to do this, we would want to expose helper API for source generators, to produce "display locations" to put in the attribute. // We would normalize to 0-indexed in this step. all our location-oriented complaints are made here, so we shouldn't need to convert back to "display location" after that point. - DeclaringCompilation.AddInterception(new InterceptsLocationAttributeData(filePath, lineNumber, characterNumber, attributeLocation), this); + DeclaringCompilation.AddInterception(new InterceptsLocationAttributeData(filePath, lineNumberZeroBased, characterNumberZeroBased, attributeLocation), this); } private void DecodeUnmanagedCallersOnlyAttribute(ref DecodeWellKnownAttributeArguments arguments) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index e0c564f56e8d5..85f0295022d8d 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -15,6 +15,7 @@ public class InterceptorsTests : CSharpTestBase // PROTOTYPE(ic): test a case where the original method has type parameter constraints. // PROTOTYPE(ic): for now we will just completely disallow type parameters in the interceptor. // PROTOTYPE(ic): test where interceptable is a constructed method or a retargeting method. + // PROTOTYPE(ic): Ensure that all `MethodSymbol.IsInterceptable` implementations have test coverage. // PROTOTYPE(ic): test where there are differences between 'scoped' modifiers and '[UnscopedRef]' attributes // PROTOTYPE(ic): Possible test cases: @@ -1346,6 +1347,45 @@ static class CExt ); } + [Fact] + public void InterceptsLocationBadPosition_07() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C { } + + static class Program + { + public static void Main() + { + var c = new C(); + c. + // comment + InterceptableMethod("call site"); + } + + [Interceptable] + public static C InterceptableMethod(this C c, string param) { Console.Write("interceptable " + param); return c; } + + [InterceptsLocation("Program.cs", 12, 13)] // intercept comment above 'InterceptableMethod' token + public static C Interceptor1(this C c, string param) { Console.Write("interceptor " + param); return c; } + } + + static class CExt + { + } + """; + // PROTOTYPE(ic): the character suggested here is wrong. What should we do to give a useful diagnostic here? + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }); + comp.VerifyEmitDiagnostics( + // Program.cs(19,6): error CS27010: The provided character number does not refer to the start of method name token 'InterceptableMethod'. Consider using character number '37' instead. + // [InterceptsLocation("Program.cs", 12, 13)] // intercept comment above 'InterceptableMethod' token + Diagnostic(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, @"InterceptsLocation(""Program.cs"", 12, 13)").WithArguments("InterceptableMethod", "37").WithLocation(19, 6) + ); + } + [Fact] public void SignatureMismatch_01() { From e65515792996d06372141148bd7024ce3ddb01b5 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Wed, 12 Apr 2023 12:52:59 -0700 Subject: [PATCH 49/49] Fix unix test failure --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 85f0295022d8d..c2c25f3df1016 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -1347,7 +1347,7 @@ static class CExt ); } - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = "PROTOTYPE(ic): diagnostic message differs depending on the size of line endings")] public void InterceptsLocationBadPosition_07() { var source = """