From 839e1abc434ead394e9b984e264d16cec5d267f9 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 15 Apr 2024 12:05:45 -0700 Subject: [PATCH] Add support for checksum-based interceptors (#72814) --- docs/features/interceptors.md | 45 +- .../CSharp/Portable/CSharpExtensions.cs | 20 + .../CSharp/Portable/CSharpResources.resx | 15 + .../Portable/Compilation/CSharpCompilation.cs | 45 +- .../Compilation/CSharpSemanticModel.cs | 29 +- .../CSharp/Portable/Errors/ErrorCode.cs | 6 + .../CSharp/Portable/Errors/ErrorFacts.cs | 5 + .../Lowering/LocalRewriter/LocalRewriter.cs | 2 +- .../LocalRewriter/LocalRewriter_Call.cs | 4 +- ...LocalRewriter_FunctionPointerInvocation.cs | 2 +- .../CSharp/Portable/PublicAPI.Unshipped.txt | 8 + .../SourceMethodSymbolWithAttributes.cs | 184 +++- .../Portable/Utilities/ContentHashComparer.cs | 30 + .../Utilities/InterceptableLocation.cs | 180 ++++ .../Portable/xlf/CSharpResources.cs.xlf | 25 + .../Portable/xlf/CSharpResources.de.xlf | 25 + .../Portable/xlf/CSharpResources.es.xlf | 25 + .../Portable/xlf/CSharpResources.fr.xlf | 25 + .../Portable/xlf/CSharpResources.it.xlf | 25 + .../Portable/xlf/CSharpResources.ja.xlf | 25 + .../Portable/xlf/CSharpResources.ko.xlf | 25 + .../Portable/xlf/CSharpResources.pl.xlf | 25 + .../Portable/xlf/CSharpResources.pt-BR.xlf | 25 + .../Portable/xlf/CSharpResources.ru.xlf | 25 + .../Portable/xlf/CSharpResources.tr.xlf | 25 + .../Portable/xlf/CSharpResources.zh-Hans.xlf | 25 + .../Portable/xlf/CSharpResources.zh-Hant.xlf | 25 + .../Semantic/Semantics/InterceptorsTests.cs | 789 +++++++++++++++++- .../SourceGeneration/GeneratorDriverTests.cs | 96 +++ .../Attributes/AttributeDescription.cs | 3 +- 30 files changed, 1734 insertions(+), 54 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs create mode 100644 src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index b96fe71e646b6..b0b8597f4f3f7 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -12,9 +12,9 @@ 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(2); // (L3,C3): prints "other interceptor 2" +c.InterceptableMethod(1); // L1: prints "interceptor 1" +c.InterceptableMethod(1); // L2: prints "other interceptor 1" +c.InterceptableMethod(2); // L3: prints "other interceptor 2" c.InterceptableMethod(1); // prints "interceptable 1" class C @@ -28,14 +28,14 @@ class C // generated code static class D { - [InterceptsLocation("Program.cs", line: /*L1*/, character: /*C1*/)] // refers to the call at (L1, C1) + [InterceptsLocation(version: 1, data: "...(refers to the call at L1)")] 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) - [InterceptsLocation("Program.cs", line: /*L3*/, character: /*C3*/)] // refers to the call at (L3, C3) + [InterceptsLocation(version: 1, data: "...(refers to the call at L2)")] + [InterceptsLocation(version: 1, data: "...(refers to the call at L3)")] public static void OtherInterceptorMethod(this C c, int param) { Console.WriteLine($"other interceptor {param}"); @@ -54,7 +54,7 @@ A method indicates that it is an *interceptor* by adding one or more `[Intercept namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute + public sealed class InterceptsLocationAttribute(int version, string data) : Attribute { } } @@ -66,29 +66,26 @@ Any "ordinary method" (i.e. with `MethodKind.Ordinary`) can have its calls inter File-local declarations of this type (`file class InterceptsLocationAttribute`) are valid and usages are recognized by the compiler when they are within the same file and compilation. A generator which needs to declare this attribute should use a file-local declaration to ensure it doesn't conflict with other generators that need to do the same thing. -#### File paths +In prior experimental releases of the feature, a well-known constructor signature `InterceptsLocation(string path, int line, int column)]` was also supported. Support for this constructor will be **dropped** prior to stable release of the feature. -The *referenced syntax tree* of an `[InterceptsLocation]` is determined by normalizing the `filePath` argument value relative to the path of the containing syntax tree of the `[InterceptsLocation]` usage, similar to how paths in `#line` directives are normalized. Let this normalized path be called `normalizedInterceptorPath`. If exactly one syntax tree in the compilation has a normalized path which matches `normalizedInterceptorPath` by ordinal string comparison, that is the *referenced syntax tree*. Otherwise, an error occurs. +#### Location encoding -`#line` directives are not considered when determining the call referenced by an `[InterceptsLocation]` attribute. In other words, the file path, line and column numbers used in `[InterceptsLocation]` are expected to refer to *unmapped* source locations. +The arguments to `[InterceptsLocation]` are: +1. a version number. The compiler may introduce new encodings for the location in the future, with corresponding new version numbers. +2. an opaque data string. This is not intended to be human-readable. -Temporarily, for compatibility purposes, when the initial matching strategy outlined above fails to match any syntax trees, we will fall back to a "compat" matching strategy which works in the following way: -- A *mapped path* of each syntax tree is determined by applying [`/pathmap`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.commandlinearguments.pathmap?view=roslyn-dotnet-4.7.0) substitution to `SyntaxTree.FilePath`. -- For a given `[InterceptsLocation]` usage, the `filePath` argument value is compared to the *mapped path* of each syntax tree using ordinal string comparison. If exactly one syntax tree matches under this comparison, that is the *referenced syntax tree*. Otherwise, an error occurs. - -Support for the "compat" strategy will be dropped prior to stable release. Tracked by https://github.com/dotnet/roslyn/issues/72265. +The "version 1" data encoding is a base64-encoded string consisting of the following data: +- 16 byte xxHash128 content checksum of the file containing the intercepted call. +- int32 in little-endian format for the position (i.e. `SyntaxNode.Position`) of the call in syntax. +- utf-8 string data containing a display file name, used for error reporting. #### 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`. - 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 to show correct usage of these, including the "translation" from 0-indexed to 1-indexed positions. +Roslyn provides a convenience API, `GetInterceptableLocation(this SemanticModel, InvocationExpressionSyntax, CancellationToken)` for inserting `[InterceptsLocation]` into generated source code. We recommend that source generators depend on this API in order to intercept calls. ### Non-invocation method usages @@ -103,7 +100,7 @@ Interceptors cannot be declared in generic types at any level of nesting. Interceptors must either be non-generic, or have arity equal to the sum of the arity of the original method's arity and containing type arities. For example: ```cs -Grandparent.Parent.Original(1, false, "a"); +Grandparent.Parent.Original(1, false, "a"); // L1 class Grandparent { @@ -115,7 +112,7 @@ class Grandparent class Interceptors { - [InterceptsLocation("Program.cs", 1, 33)] + [InterceptsLocation(1, "..(refers to call at L1)")] public static void Interceptor(T1 t1, T2 t2, T3 t3) { } } ``` @@ -136,13 +133,13 @@ static class Program { public static void M(T2 t) { - C.InterceptableMethod(t); + C.InterceptableMethod(t); // L1 } } static class D { - [InterceptsLocation("Program.cs", 12, 11)] + [InterceptsLocation(1, "..(refers to call at L1)")] public static void Interceptor1(T2 t) => throw null!; } ``` diff --git a/src/Compilers/CSharp/Portable/CSharpExtensions.cs b/src/Compilers/CSharp/Portable/CSharpExtensions.cs index c42af7489034b..745c036c18d99 100644 --- a/src/Compilers/CSharp/Portable/CSharpExtensions.cs +++ b/src/Compilers/CSharp/Portable/CSharpExtensions.cs @@ -1638,6 +1638,26 @@ public static Conversion ClassifyConversion(this SemanticModel? semanticModel, i var csModel = semanticModel as CSharpSemanticModel; return csModel?.GetInterceptorMethod(node, cancellationToken); } + + /// + /// If cannot be intercepted syntactically, returns null. + /// Otherwise, returns an instance which can be used to intercept the call denoted by . + /// + [Experimental(RoslynExperiments.Interceptors, UrlFormat = RoslynExperiments.Interceptors_Url)] + public static InterceptableLocation? GetInterceptableLocation(this SemanticModel? semanticModel, InvocationExpressionSyntax node, CancellationToken cancellationToken = default) + { + var csModel = semanticModel as CSharpSemanticModel; + return csModel?.GetInterceptableLocation(node, cancellationToken); + } + + /// + /// Gets an attribute list syntax consisting of an InterceptsLocationAttribute, which intercepts the call referenced by parameter . + /// + [Experimental(RoslynExperiments.Interceptors, UrlFormat = RoslynExperiments.Interceptors_Url)] + public static string GetInterceptsLocationAttributeSyntax(this InterceptableLocation location) + { + return $"""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute({location.Version}, "{location.Data}")]"""; + } #endregion } } diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 35d9967120efb..cd8a8755867fc 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7908,4 +7908,19 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Cannot perform a dynamic invocation on an expression with type '{0}'. + + The data argument to InterceptsLocationAttribute is not in the correct format. + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index f71459b318246..04b74a1dbd441 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -160,8 +161,12 @@ internal Conversions Conversions private ImmutableSegmentedDictionary> _mappedPathToSyntaxTree; /// Lazily caches SyntaxTrees by their path. Used to look up the syntax tree referenced by an interceptor. + /// Must be removed prior to interceptors stable release. private ImmutableSegmentedDictionary> _pathToSyntaxTree; + /// Lazily caches SyntaxTrees by their xxHash128 checksum. Used to look up the syntax tree referenced by an interceptor. + private ImmutableSegmentedDictionary, OneOrMany> _contentHashToSyntaxTree; + public override string Language { get @@ -1075,6 +1080,32 @@ ImmutableSegmentedDictionary> computeMappedPathToS } } + internal OneOrMany GetSyntaxTreesByContentHash(ReadOnlyMemory contentHash) + { + Debug.Assert(contentHash.Length == InterceptableLocation1.ContentHashLength); + + var contentHashToSyntaxTree = _contentHashToSyntaxTree; + if (contentHashToSyntaxTree.IsDefault) + { + RoslynImmutableInterlocked.InterlockedInitialize(ref _contentHashToSyntaxTree, computeHashToSyntaxTree()); + contentHashToSyntaxTree = _contentHashToSyntaxTree; + } + + return contentHashToSyntaxTree.TryGetValue(contentHash, out var value) ? value : OneOrMany.Empty; + + ImmutableSegmentedDictionary, OneOrMany> computeHashToSyntaxTree() + { + var builder = ImmutableSegmentedDictionary.CreateBuilder, OneOrMany>(ContentHashComparer.Instance); + foreach (var tree in SyntaxTrees) + { + var text = tree.GetText(); + var hash = text.GetContentHash().AsMemory(); + builder[hash] = builder.TryGetValue(hash, out var existing) ? existing.Add(tree) : OneOrMany.Create(tree); + } + return builder.ToImmutable(); + } + } + internal OneOrMany GetSyntaxTreesByPath(string path) { // We could consider storing this on SyntaxAndDeclarationManager instead, and updating it incrementally. @@ -2399,15 +2430,15 @@ internal void AddModuleInitializerMethod(MethodSymbol method) internal bool InterceptorsDiscoveryComplete; // NB: the 'Many' case for these dictionary values means there are duplicates. An error is reported for this after binding. - private ConcurrentDictionary<(string FilePath, int Line, int Character), OneOrMany<(Location AttributeLocation, MethodSymbol Interceptor)>>? _interceptions; + private ConcurrentDictionary<(string FilePath, int Position), OneOrMany<(Location AttributeLocation, MethodSymbol Interceptor)>>? _interceptions; - internal void AddInterception(string filePath, int line, int character, Location attributeLocation, MethodSymbol interceptor) + internal void AddInterception(string filePath, int position, Location attributeLocation, MethodSymbol interceptor) { Debug.Assert(!_declarationDiagnosticsFrozen); Debug.Assert(!InterceptorsDiscoveryComplete); var dictionary = LazyInitializer.EnsureInitialized(ref _interceptions); - dictionary.AddOrUpdate((filePath, line, character), + dictionary.AddOrUpdate((filePath, position), addValueFactory: static (key, newValue) => OneOrMany.Create(newValue), updateValueFactory: static (key, existingValues, newValue) => { @@ -2427,9 +2458,9 @@ internal void AddInterception(string filePath, int line, int character, Location factoryArgument: (AttributeLocation: attributeLocation, Interceptor: interceptor)); } - internal (Location AttributeLocation, MethodSymbol Interceptor)? TryGetInterceptor(Location? callLocation) + internal (Location AttributeLocation, MethodSymbol Interceptor)? TryGetInterceptor(SimpleNameSyntax? node) { - if (callLocation is null || !callLocation.IsInSource) + if (node is null) { return null; } @@ -2440,9 +2471,7 @@ internal void AddInterception(string filePath, int line, int character, Location return null; } - var callLineColumn = callLocation.GetLineSpan().Span.Start; - var key = (callLocation.SourceTree.FilePath, callLineColumn.Line, callLineColumn.Character); - + var key = (node.SyntaxTree.FilePath, node.Position); if (_interceptions.TryGetValue(key, out var interceptionsAtAGivenLocation) && interceptionsAtAGivenLocation is [var oneInterception]) { return oneInterception; diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs index 4a54eece1b5e2..b79df55c6af72 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs @@ -5208,13 +5208,40 @@ protected sealed override ISymbol GetDeclaredSymbolCore(SyntaxNode node, Cancell CheckSyntaxNode(node); - if (node.GetInterceptableNameSyntax() is { } nameSyntax && Compilation.TryGetInterceptor(nameSyntax.GetLocation()) is (_, MethodSymbol interceptor)) + if (node.GetInterceptableNameSyntax() is { } nameSyntax && Compilation.TryGetInterceptor(nameSyntax) is (_, MethodSymbol interceptor)) { return interceptor.GetPublicSymbol(); } return null; } + +#pragma warning disable RSEXPERIMENTAL002 // Internal usage of experimental API + public InterceptableLocation? GetInterceptableLocation(InvocationExpressionSyntax node, CancellationToken cancellationToken) + { + CheckSyntaxNode(node); + if (node.GetInterceptableNameSyntax() is not { } nameSyntax) + { + return null; + } + + return GetInterceptableLocationInternal(nameSyntax, cancellationToken); + } + + // Factored out for ease of test authoring, especially for scenarios involving unsupported syntax. + internal InterceptableLocation GetInterceptableLocationInternal(SyntaxNode nameSyntax, CancellationToken cancellationToken) + { + var tree = nameSyntax.SyntaxTree; + var text = tree.GetText(cancellationToken); + var path = tree.FilePath; + var checksum = text.GetContentHash(); + + var lineSpan = nameSyntax.Location.GetLineSpan().Span.Start; + var lineNumberOneIndexed = lineSpan.Line + 1; + var characterNumberOneIndexed = lineSpan.Character + 1; + + return new InterceptableLocation1(checksum, path, nameSyntax.Position, lineNumberOneIndexed, characterNumberOneIndexed); + } #nullable disable protected static SynthesizedPrimaryConstructor TryGetSynthesizedPrimaryConstructor(TypeDeclarationSyntax node, NamedTypeSymbol type) diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 0b141c96fe9f6..15fee745c96a3 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2303,6 +2303,12 @@ internal enum ErrorCode ERR_NoModifiersOnUsing = 9229, ERR_CannotDynamicInvokeOnExpression = 9230, + ERR_InterceptsLocationDataInvalidFormat = 9231, + ERR_InterceptsLocationUnsupportedVersion = 9232, + ERR_InterceptsLocationDuplicateFile = 9233, + ERR_InterceptsLocationFileNotFound = 9234, + ERR_InterceptsLocationDataInvalidPosition = 9235, + #endregion // Note: you will need to do the following after adding errors: diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 2342c333dfb1f..e634deae8b73d 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -2432,6 +2432,11 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_ParamsCollectionMissingConstructor: case ErrorCode.ERR_NoModifiersOnUsing: case ErrorCode.ERR_CannotDynamicInvokeOnExpression: + case ErrorCode.ERR_InterceptsLocationDataInvalidFormat: + case ErrorCode.ERR_InterceptsLocationUnsupportedVersion: + case ErrorCode.ERR_InterceptsLocationDuplicateFile: + case ErrorCode.ERR_InterceptsLocationFileNotFound: + case ErrorCode.ERR_InterceptsLocationDataInvalidPosition: 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.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs index 30707d0245d8f..59cd4fd4afe8a 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs @@ -247,7 +247,7 @@ private PEModuleBuilder? EmitModule { Debug.Assert(!nameofOperator.WasCompilerGenerated); var nameofIdentiferSyntax = (IdentifierNameSyntax)((InvocationExpressionSyntax)nameofOperator.Syntax).Expression; - if (this._compilation.TryGetInterceptor(nameofIdentiferSyntax.Location) is not null) + if (this._compilation.TryGetInterceptor(nameofIdentiferSyntax) is not null) { this._diagnostics.Add(ErrorCode.ERR_InterceptorCannotInterceptNameof, nameofIdentiferSyntax.Location); } diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 956ff4e6071c1..a63910f4e0572 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -141,15 +141,13 @@ private void InterceptCallAndAdjustArguments( bool invokedAsExtensionMethod, Syntax.SimpleNameSyntax? nameSyntax) { - var interceptableLocation = nameSyntax?.Location; - if (this._compilation.TryGetInterceptor(interceptableLocation) is not var (attributeLocation, interceptor)) + if (this._compilation.TryGetInterceptor(nameSyntax) is not var (attributeLocation, interceptor)) { // The call was not intercepted. return; } Debug.Assert(nameSyntax != null); - Debug.Assert(interceptableLocation != null); Debug.Assert(interceptor.IsDefinition); Debug.Assert(!interceptor.ContainingType.IsGenericType); diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_FunctionPointerInvocation.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_FunctionPointerInvocation.cs index e0968a7089755..b98dac8930faf 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_FunctionPointerInvocation.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_FunctionPointerInvocation.cs @@ -35,7 +35,7 @@ internal sealed partial class LocalRewriter Debug.Assert(discardedReceiver is null); - if (node.InterceptableNameSyntax is { } nameSyntax && this._compilation.TryGetInterceptor(nameSyntax.Location) is var (attributeLocation, _)) + if (node.InterceptableNameSyntax is { } nameSyntax && this._compilation.TryGetInterceptor(nameSyntax) is var (attributeLocation, _)) { this._diagnostics.Add(ErrorCode.ERR_InterceptableMethodMustBeOrdinary, attributeLocation, nameSyntax.Identifier.ValueText); } diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index a7eede3cf18d3..e2eac632d971c 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -1,5 +1,9 @@ Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp12 = 1200 -> Microsoft.CodeAnalysis.CSharp.LanguageVersion Microsoft.CodeAnalysis.CSharp.Conversion.IsCollectionExpression.get -> bool +[RSEXPERIMENTAL002]Microsoft.CodeAnalysis.CSharp.InterceptableLocation +[RSEXPERIMENTAL002]abstract Microsoft.CodeAnalysis.CSharp.InterceptableLocation.Data.get -> string! +[RSEXPERIMENTAL002]abstract Microsoft.CodeAnalysis.CSharp.InterceptableLocation.GetDisplayLocation() -> string! +[RSEXPERIMENTAL002]abstract Microsoft.CodeAnalysis.CSharp.InterceptableLocation.Version.get -> int Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax.ReadOnlyKeyword.get -> Microsoft.CodeAnalysis.SyntaxToken Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax.Update(Microsoft.CodeAnalysis.SyntaxToken refKindKeyword, Microsoft.CodeAnalysis.SyntaxToken readOnlyKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax! type) -> Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax! Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax.WithReadOnlyKeyword(Microsoft.CodeAnalysis.SyntaxToken readOnlyKeyword) -> Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax! @@ -17,4 +21,8 @@ static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetElementConversion(this static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.CreateTokenParser(Microsoft.CodeAnalysis.Text.SourceText! sourceText, Microsoft.CodeAnalysis.CSharp.CSharpParseOptions? options = null) -> Microsoft.CodeAnalysis.CSharp.SyntaxTokenParser! static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.CrefParameter(Microsoft.CodeAnalysis.SyntaxToken refKindKeyword, Microsoft.CodeAnalysis.SyntaxToken readOnlyKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax! type) -> Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax! [RSEXPERIMENTAL001]Microsoft.CodeAnalysis.CSharp.CSharpCompilation.GetSemanticModel(Microsoft.CodeAnalysis.SyntaxTree! syntaxTree, Microsoft.CodeAnalysis.SemanticModelOptions options) -> Microsoft.CodeAnalysis.SemanticModel! +[RSEXPERIMENTAL002]override abstract Microsoft.CodeAnalysis.CSharp.InterceptableLocation.Equals(object? obj) -> bool +[RSEXPERIMENTAL002]override abstract Microsoft.CodeAnalysis.CSharp.InterceptableLocation.GetHashCode() -> int [RSEXPERIMENTAL002]static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetInterceptorMethod(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax! node, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.IMethodSymbol? +[RSEXPERIMENTAL002]static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetInterceptableLocation(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax! node, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.CSharp.InterceptableLocation? +[RSEXPERIMENTAL002]static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetInterceptsLocationAttributeSyntax(this Microsoft.CodeAnalysis.CSharp.InterceptableLocation! location) -> string! diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index c23defaca34cc..a4c5ed6fff70e 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -5,11 +5,13 @@ #nullable disable using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -942,21 +944,184 @@ private void DecodeModuleInitializerAttribute(DecodeWellKnownAttributeArguments< private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments arguments) { - Debug.Assert(arguments.AttributeSyntaxOpt is object); Debug.Assert(!arguments.Attribute.HasErrors); - var attributeData = arguments.Attribute; - var attributeArguments = attributeData.CommonConstructorArguments; - if (attributeArguments is not [ + var constructorArguments = arguments.Attribute.CommonConstructorArguments; + if (constructorArguments is [ { Type.SpecialType: SpecialType.System_String }, { 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(); + DecodeInterceptsLocationAttributeExperimentalCompat(arguments, attributeFilePath: (string?)constructorArguments[0].Value, lineNumberOneBased, characterNumberOneBased); + } + else + { + Debug.Assert(arguments.Attribute.AttributeConstructor.Parameters is [{ Type.SpecialType: SpecialType.System_Int32 }, { Type.SpecialType: SpecialType.System_String }]); + DecodeInterceptsLocationChecksumBased(arguments, version: (int)constructorArguments[0].Value!, data: (string?)constructorArguments[1].Value); + } + } + + private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArguments arguments, int version, string? data) + { + var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; + Debug.Assert(arguments.AttributeSyntaxOpt is not null); + var attributeNameSyntax = arguments.AttributeSyntaxOpt.Name; // used for reporting diagnostics + var attributeLocation = attributeNameSyntax.Location; + + if (version != 1) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationUnsupportedVersion, attributeLocation, version); + return; + } + + if (InterceptableLocation1.Decode(data) is not var (hash, position, displayFileName)) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); + return; + } + + var interceptorsNamespaces = ((CSharpParseOptions)attributeNameSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; + var thisNamespaceNames = getNamespaceNames(this); + var foundAnyMatch = interceptorsNamespaces.Any(static (ns, thisNamespaceNames) => isDeclaredInNamespace(thisNamespaceNames, ns), thisNamespaceNames); + if (!foundAnyMatch) + { + reportFeatureNotEnabled(diagnostics, attributeLocation, thisNamespaceNames); + thisNamespaceNames.Free(); + return; + } + thisNamespaceNames.Free(); + + if (ContainingType.IsGenericType) + { + diagnostics.Add(ErrorCode.ERR_InterceptorContainingTypeCannotBeGeneric, attributeLocation, this); + return; + } + + if (MethodKind != MethodKind.Ordinary) + { + diagnostics.Add(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, attributeLocation); + return; + } + + Debug.Assert(_lazyCustomAttributesBag.IsEarlyDecodedWellKnownAttributeDataComputed); + var unmanagedCallersOnly = this.GetUnmanagedCallersOnlyAttributeData(forceComplete: false); + if (unmanagedCallersOnly != null) + { + diagnostics.Add(ErrorCode.ERR_InterceptorCannotUseUnmanagedCallersOnly, attributeLocation); + return; + } + + var matchingTrees = DeclaringCompilation.GetSyntaxTreesByContentHash(hash); + if (matchingTrees.Count > 1) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDuplicateFile, attributeLocation, displayFileName); + return; + } + + if (matchingTrees.Count == 0) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationFileNotFound, attributeLocation, displayFileName); + return; + } + + Debug.Assert(matchingTrees.Count == 1); + SyntaxTree? matchingTree = matchingTrees[0]; + + var root = matchingTree.GetRoot(); + if (position < 0 || position > root.EndPosition) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, attributeLocation, displayFileName); + return; + } + + var referencedLines = matchingTree.GetText().Lines; + var referencedLineCount = referencedLines.Count; + var referencedToken = root.FindToken(position); + 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; + 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; + } + + if (position != referencedToken.Position) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, attributeLocation, displayFileName); + return; } + DeclaringCompilation.AddInterception(matchingTree.FilePath, position, attributeLocation, this); + + // Caller must free the returned builder. + static ArrayBuilder getNamespaceNames(SourceMethodSymbolWithAttributes @this) + { + var namespaceNames = ArrayBuilder.GetInstance(); + for (var containingNamespace = @this.ContainingNamespace; containingNamespace?.IsGlobalNamespace == false; containingNamespace = containingNamespace.ContainingNamespace) + namespaceNames.Add(containingNamespace.Name); + // order outermost->innermost + // e.g. for method MyApp.Generated.Interceptors.MyInterceptor(): ["MyApp", "Generated", "Interceptors"] + namespaceNames.ReverseContents(); + return namespaceNames; + } + + static bool isDeclaredInNamespace(ArrayBuilder thisNamespaceNames, ImmutableArray namespaceSegments) + { + Debug.Assert(namespaceSegments.Length > 0); + if (namespaceSegments is ["global"]) + { + return true; + } + + if (namespaceSegments.Length > thisNamespaceNames.Count) + { + // the enabled NS has more components than interceptor's NS, so it will never match. + return false; + } + + for (var i = 0; i < namespaceSegments.Length; i++) + { + if (namespaceSegments[i] != thisNamespaceNames[i]) + { + return false; + } + } + return true; + } + + static void reportFeatureNotEnabled(BindingDiagnosticBag diagnostics, Location attributeLocation, ArrayBuilder namespaceNames) + { + if (namespaceNames.Count == 0) + { + diagnostics.Add(ErrorCode.ERR_InterceptorGlobalNamespace, attributeLocation); + } + else + { + var recommendedProperty = $"$(InterceptorsPreviewNamespaces);{string.Join(".", namespaceNames)}"; + diagnostics.Add(ErrorCode.ERR_InterceptorsFeatureNotEnabled, attributeLocation, recommendedProperty); + } + } + } + + // https://github.com/dotnet/roslyn/issues/72265: Remove support for path-based interceptors prior to stable release. + private void DecodeInterceptsLocationAttributeExperimentalCompat( + DecodeWellKnownAttributeArguments arguments, + string? attributeFilePath, + int lineNumberOneBased, + int characterNumberOneBased) + { var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; var attributeSyntax = arguments.AttributeSyntaxOpt; + Debug.Assert(attributeSyntax is object); var attributeLocation = attributeSyntax.Location; const int filePathParameterIndex = 0; const int lineNumberParameterIndex = 1; @@ -973,7 +1138,7 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments } thisNamespaceNames.Free(); - var attributeFilePath = (string?)attributeArguments[0].Value; + var attributeData = arguments.Attribute; if (attributeFilePath is null) { diagnostics.Add(ErrorCode.ERR_InterceptorFilePathCannotBeNull, attributeData.GetAttributeArgumentLocation(filePathParameterIndex)); @@ -1100,14 +1265,15 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments } // Did they actually refer to the start of the token, not the middle, or in trivia? - if (referencedPosition != referencedToken.Span.Start) + // NB: here we don't want the provided position to refer to the start of token's leading trivia, in the checksum-based way we *do* want it to refer to the start of leading trivia (i.e. the Position) + if (referencedPosition != referencedToken.SpanStart) { var linePositionZeroBased = referencedToken.GetLocation().GetLineSpan().StartLinePosition; diagnostics.Add(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, attributeLocation, referencedToken.Text, linePositionZeroBased.Line + 1, linePositionZeroBased.Character + 1); return; } - DeclaringCompilation.AddInterception(matchingTree.FilePath, lineNumberZeroBased, characterNumberZeroBased, attributeLocation, this); + DeclaringCompilation.AddInterception(matchingTree.FilePath, referencedToken.Position, attributeLocation, this); // Caller must free the returned builder. ArrayBuilder getNamespaceNames() diff --git a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs new file mode 100644 index 0000000000000..4bc6b50362f96 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs @@ -0,0 +1,30 @@ +// 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.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp; + +internal sealed class ContentHashComparer : IEqualityComparer> +{ + public static ContentHashComparer Instance { get; } = new ContentHashComparer(); + + private ContentHashComparer() { } + + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + { + return x.Span.SequenceEqual(y.Span); + } + + public int GetHashCode(ReadOnlyMemory obj) + { + // We expect the content hash to be well-mixed. + // Therefore simply reading the first 4 bytes of it results in an adequate hash code. + return BinaryPrimitives.ReadInt32LittleEndian(obj.Span); + } +} diff --git a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs new file mode 100644 index 0000000000000..4f3a23634d307 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -0,0 +1,180 @@ +// 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.Buffers.Binary; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Cci; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp; + +[Experimental(RoslynExperiments.Interceptors, UrlFormat = RoslynExperiments.Interceptors_Url)] +public abstract class InterceptableLocation +{ + private protected InterceptableLocation() { } + + /// + /// The version of the location encoding. Used as an argument to 'InterceptsLocationAttribute'. + /// + public abstract int Version { get; } + + /// + /// Opaque data which references a call when used as an argument to 'InterceptsLocationAttribute'. + /// The value does not require escaping, i.e. it is valid in a string literal when wrapped in " (double-quote) characters. + /// + public abstract string Data { get; } + + /// + /// Gets a human-readable representation of the location, suitable for including in comments in generated code. + /// + public abstract string GetDisplayLocation(); + + public abstract override bool Equals(object? obj); + public abstract override int GetHashCode(); +} + +#pragma warning disable RSEXPERIMENTAL002 // internal usage of experimental API +/// +/// Version 1 of the InterceptableLocation encoding. +/// +internal sealed class InterceptableLocation1 : InterceptableLocation +{ + internal const int ContentHashLength = 16; + private static readonly UTF8Encoding s_encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + private readonly ImmutableArray _checksum; + private readonly string _path; + private readonly int _position; + private readonly int _lineNumberOneIndexed; + private readonly int _characterNumberOneIndexed; + private string? _lazyData; + + internal InterceptableLocation1(ImmutableArray checksum, string path, int position, int lineNumberOneIndexed, int characterNumberOneIndexed) + { + Debug.Assert(checksum.Length == ContentHashLength); + Debug.Assert(path is not null); + Debug.Assert(position >= 0); + Debug.Assert(lineNumberOneIndexed > 0); + Debug.Assert(characterNumberOneIndexed > 0); + + _checksum = checksum; + _path = path; + _position = position; + _lineNumberOneIndexed = lineNumberOneIndexed; + _characterNumberOneIndexed = characterNumberOneIndexed; + } + + public override string GetDisplayLocation() + { + // e.g. `C:\project\src\Program.cs(12,34)` + return $"{_path}({_lineNumberOneIndexed},{_characterNumberOneIndexed})"; + } + + public override string ToString() => GetDisplayLocation(); + + public override int Version => 1; + public override string Data + { + get + { + if (_lazyData is null) + _lazyData = makeData(); + + return _lazyData; + + string makeData() + { + var builder = PooledBlobBuilder.GetInstance(); + builder.WriteBytes(_checksum, start: 0, 16); + builder.WriteInt32(_position); + + var displayFileName = Path.GetFileName(_path); + builder.WriteUTF8(displayFileName); + + var bytes = builder.ToArray(); + builder.Free(); + return Convert.ToBase64String(bytes); + } + } + } + + internal static (ReadOnlyMemory checksum, int position, string displayFileName)? Decode(string? data) + { + if (data is null) + { + return null; + } + + byte[] bytes; + try + { + bytes = Convert.FromBase64String(data); + } + catch (FormatException) + { + return null; + } + + // format: + // - 16 bytes of target file content hash (xxHash128) + // - int32 position (little endian) + // - utf-8 display filename + const int hashIndex = 0; + const int hashSize = 16; + const int positionIndex = hashIndex + hashSize; + const int positionSize = sizeof(int); + const int displayNameIndex = positionIndex + positionSize; + const int minLength = displayNameIndex; + + if (bytes.Length < minLength) + { + return null; + } + + var hash = bytes.AsMemory(start: hashIndex, length: hashSize); + var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: positionIndex)); + + string displayFileName; + try + { + displayFileName = s_encoding.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); + } + catch (ArgumentException) + { + return null; + } + + return (hash, position, displayFileName); + } + + // Note: the goal of implementing equality here is so that incremental state tables etc. can detect and use it. + // This encoding which uses the checksum of the referenced file may not be stable across incremental runs in practice, but it seems correct in principle to implement equality here anyway. + public override bool Equals(object? obj) + { + if ((object)this == obj) + return true; + + return obj is InterceptableLocation1 other + && _checksum.SequenceEqual(other._checksum) + && _path == other._path + && _position == other._position + && _lineNumberOneIndexed == other._lineNumberOneIndexed + && _characterNumberOneIndexed == other._characterNumberOneIndexed; + } + + public override int GetHashCode() + { + // Use only the _checksum and _position in the hash as these are the most distinctive fields of the location. + // i.e. if these are equal across instances, then other fields are likely to be equal as well. + return Hash.Combine( + BinaryPrimitives.ReadInt32LittleEndian(_checksum.AsSpan()), + _position); + } +} diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 94e72ea2f90a1..9cb6c6666dea2 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -1102,6 +1102,31 @@ Experimentální funkce interceptors není v tomto oboru názvů povolená. Přidejte do projektu {0}. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 002d1aceb7570..5350d1c59acab 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -1102,6 +1102,31 @@ Das experimentelle Feature "Interceptors" ist nicht in diesem Namespace aktiviert. Fügen Sie Ihrem Projekt "{0}" hinzu. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 700e8f5912984..2a449abc1f16c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -1102,6 +1102,31 @@ La característica experimental "interceptores" no está habilitada en este espacio de nombres. Agregue '{0}' al proyecto. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 b5d7cacbfdb09..e39c57737603f 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -1102,6 +1102,31 @@ La fonctionnalité expérimentale « intercepteurs » n'est pas activée dans cet espace de noms. Ajoutez « {0} » à votre projet. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 920818aa35c52..d3378e3228a1f 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -1102,6 +1102,31 @@ La funzionalità sperimentale 'intercettori' non è abilitata in questo spazio dei nomi. Aggiungere '{0}' al progetto. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 9ccc403201939..88b8066444024 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -1102,6 +1102,31 @@ 'インターセプター' の実験的な機能は、この名前空間では有効になっていません。プロジェクトに '{0}' を追加します。 + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 736c1e7ed0f2c..b964b1a1d24fd 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -1102,6 +1102,31 @@ 이 네임스페이스에서는 '인터셉터' 실험적 기능을 사용할 수 없습니다. 프로젝트에 '{0}'을(를) 추가하세요. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 e05459fae3648..c7aceef3e3fc0 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -1102,6 +1102,31 @@ Funkcja eksperymentalna „interceptorów” nie jest włączona w tej przestrzeni nazw. Dodaj „{0}” do swojego projektu. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 528dfbfdf5076..eca4a0f9e327d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -1102,6 +1102,31 @@ O recurso experimental “interceptadores” não está habilitado neste namespace. Adicione “{0}” ao seu projeto. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 14c4a7c5ce8fc..f00022bc0283a 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -1102,6 +1102,31 @@ Экспериментальная функция "перехватчики" не включена в этом пространстве имен. Добавьте "{0}" в свой проект. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 45e26b5b7c14f..3104df55006e0 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -1102,6 +1102,31 @@ 'Engelleyiciler' deneysel özelliği bu ad alanında etkin değil. Projenize '{0}' ekleyin. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 bd7eb252201c2..4a501f24b6625 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -1102,6 +1102,31 @@ 此命名空间中未启用“拦截器”实验性功能。请将“{0}”添加到项目。 + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 bf6487794259d..94d77f41046f6 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -1102,6 +1102,31 @@ 未在此命名空間中啟用「攔截器」實驗功能。將 '{0}' 新增至您的專案。 + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + The data argument to InterceptsLocationAttribute refers to an invalid position in file '{0}'. + + + + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + Cannot intercept a call in file '{0}' because it is duplicated elsewhere in the compilation. + + + + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + Cannot intercept a call in file '{0}' because a matching file was not found in the compilation. + + + + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + Version '{0}' of the interceptors format is not supported. The latest supported version is '1'. + + '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 b1cf3f2f8a850..63b63f19b1d28 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Reflection.Metadata; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; @@ -21,20 +22,21 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; public class InterceptorsTests : CSharpTestBase { - private static readonly (string, string) s_attributesSource = (""" + private static readonly (string text, string path) s_attributesSource = (""" namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] public sealed class InterceptsLocationAttribute : Attribute { - public InterceptsLocationAttribute(string filePath, int line, int character) - { - } + public InterceptsLocationAttribute(string filePath, int line, int character) { } + public InterceptsLocationAttribute(int version, string data) { } } """, "attributes.cs"); private static readonly CSharpParseOptions RegularWithInterceptors = TestOptions.Regular.WithFeature("InterceptorsPreviewNamespaces", "global"); + private static readonly SyntaxTree s_attributesTree = CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors); + [Fact] public void FeatureFlag() { @@ -127,6 +129,69 @@ class D verifier.VerifyDiagnostics(); } + [Fact] + public void FeatureFlag_Granular_Checksum_01() + { + test(TestOptions.Regular.WithFeature("InterceptorsPreviewNamespaces", "NS"), expectedOutput: null, + // Interceptors.cs(7,10): error CS9137: The 'interceptors' experimental feature is not enabled in this namespace. Add '$(InterceptorsPreviewNamespaces);NS1' to your project. + // [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "eY+urAo7Kg2rsKgGSGjShwIAAABQcm9ncmFtLmNz")] + Diagnostic(ErrorCode.ERR_InterceptorsFeatureNotEnabled, "global::System.Runtime.CompilerServices.InterceptsLocationAttribute").WithArguments("$(InterceptorsPreviewNamespaces);NS1").WithLocation(7, 10)); + + test(TestOptions.Regular.WithFeature("InterceptorsPreviewNamespaces", "NS1.NS2"), expectedOutput: null, + // Interceptors.cs(7,10): error CS9137: The 'interceptors' experimental feature is not enabled in this namespace. Add '$(InterceptorsPreviewNamespaces);NS1' to your project. + // [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "eY+urAo7Kg2rsKgGSGjShwIAAABQcm9ncmFtLmNz")] + Diagnostic(ErrorCode.ERR_InterceptorsFeatureNotEnabled, "global::System.Runtime.CompilerServices.InterceptsLocationAttribute").WithArguments("$(InterceptorsPreviewNamespaces);NS1").WithLocation(7, 10)); + + test(TestOptions.Regular.WithFeature("InterceptorsPreviewNamespaces", "NS1"), expectedOutput: "1"); + + test(TestOptions.Regular.WithFeature("InterceptorsPreviewNamespaces", "NS1;NS2"), expectedOutput: "1"); + + void test(CSharpParseOptions options, string? expectedOutput, params DiagnosticDescription[] expected) + { + var source = CSharpTestSource.Parse(""" + C.M(); + + class C + { + public static void M() => throw null!; + } + """, path: "Program.cs", options); + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var model = comp.GetSemanticModel(source); + var invocation = source.GetRoot().DescendantNodes().OfType().Single(); + var interceptableLocation = model.GetInterceptableLocation(invocation)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System; + + namespace NS1 + { + class D + { + {{interceptableLocation.GetInterceptsLocationAttributeSyntax()}} + public static void M() => Console.Write(1); + } + } + """, path: "Interceptors.cs", options); + var attributesTree = CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, options: options); + + comp = CreateCompilation([source, interceptors, attributesTree]); + + if (expectedOutput == null) + { + comp.VerifyEmitDiagnostics(expected); + } + else + { + CompileAndVerify(comp, expectedOutput: expectedOutput) + .VerifyDiagnostics(expected); + } + } + } + [Fact] public void FeatureFlag_Granular_02() { @@ -1790,6 +1855,62 @@ public static string Prop ); } + [Fact] + public void InterceptsLocation_BadMethodKind_Checksum() + { + var source = CSharpTestSource.Parse(""" + class Program + { + public static void InterceptableMethod(string param) { } + + public static void Main() + { + InterceptableMethod(""); + } + } + """, "Program.cs", RegularWithInterceptors); + + var comp = CreateCompilation(source); + var invocation = source.GetRoot().DescendantNodes().OfType().Single(); + var model = comp.GetSemanticModel(source); + var location = model.GetInterceptableLocation(invocation)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System.Runtime.CompilerServices; + + class C + { + static void M() + { + Interceptor1(""); + var lambda = [InterceptsLocation({{location.Version}}, "{{location.Data}}")] (string param) => { }; // 1 + + [InterceptsLocation({{location.Version}}, "{{location.Data}}")] // 2 + static void Interceptor1(string param) { } + } + + public static string Prop + { + [InterceptsLocation({{location.Version}}, "{{location.Data}}")] // 3 + set { } + } + } + """, "Interceptors.cs", RegularWithInterceptors); + + comp = CreateCompilation([source, interceptors, s_attributesTree]); + comp.VerifyDiagnostics( + // Interceptors.cs(8,23): error CS9146: An interceptor method must be an ordinary member method. + // var lambda = [InterceptsLocation(1, "OjpNlan67EMibFykRLWBLXgAAABQcm9ncmFtLmNz")] (string param) => { }; // 1 + Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, "InterceptsLocation").WithLocation(8, 23), + // Interceptors.cs(10,10): error CS9146: An interceptor method must be an ordinary member method. + // [InterceptsLocation(1, "OjpNlan67EMibFykRLWBLXgAAABQcm9ncmFtLmNz")] // 2 + Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, "InterceptsLocation").WithLocation(10, 10), + // Interceptors.cs(16,10): error CS9146: An interceptor method must be an ordinary member method. + // [InterceptsLocation(1, "OjpNlan67EMibFykRLWBLXgAAABQcm9ncmFtLmNz")] // 3 + Diagnostic(ErrorCode.ERR_InterceptorMethodMustBeOrdinary, "InterceptsLocation").WithLocation(16, 10) + ); + } + [Fact] public void InterceptableMethod_BadMethodKind_01() { @@ -1829,6 +1950,64 @@ static void Interceptor1() { } ); } + [Fact] + public void InterceptableMethod_BadMethodKind_Checksum_01() + { + var source = CSharpTestSource.Parse(""" + class Program + { + public static void Main() + { + // property + _ = Prop; // 1 ('Prop') + + // constructor + new Program(); // 2 ('new'), 3 ('Program') + } + + public static int Prop { get; } + } + """, "Program.cs", options: RegularWithInterceptors); + + var comp = CreateCompilation(source); + var model = (CSharpSemanticModel)comp.GetSemanticModel(source); + var root = source.GetRoot(); + + var node1 = root.DescendantNodes().First(node => node is IdentifierNameSyntax name && name.Identifier.Text == "Prop"); + var location1 = model.GetInterceptableLocationInternal(node1, cancellationToken: default); + + var node2 = root.DescendantNodes().Single(node => node is ObjectCreationExpressionSyntax); + var location2 = model.GetInterceptableLocationInternal(node2, cancellationToken: default); + + var node3 = root.DescendantNodes().Last(node => node is IdentifierNameSyntax name && name.Identifier.Text == "Program"); + var location3 = model.GetInterceptableLocationInternal(node3, cancellationToken: default); + + var interceptors = CSharpTestSource.Parse($$""" + using System.Runtime.CompilerServices; + + class C + { + [InterceptsLocation({{location1.Version}}, "{{location1.Data}}")] // 1 + [InterceptsLocation({{location2.Version}}, "{{location2.Data}}")] // 2 + [InterceptsLocation({{location3.Version}}, "{{location3.Data}}")] // 3 + static void Interceptor1() { } + } + """, "Interceptors.cs", RegularWithInterceptors); + + comp = CreateCompilation([source, interceptors, s_attributesTree]); + comp.VerifyDiagnostics( + // Interceptors.cs(5,6): error CS9151: Possible method name 'Prop' cannot be intercepted because it is not being invoked. + // [InterceptsLocation(1, "hD44wQkJk1har7RM7oznpFkAAABQcm9ncmFtLmNz")] // 1 + Diagnostic(ErrorCode.ERR_InterceptorNameNotInvoked, "InterceptsLocation").WithArguments("Prop").WithLocation(5, 6), + // Interceptors.cs(6,6): error CS9141: The provided line and character number does not refer to an interceptable method name, but rather to token 'new'. + // [InterceptsLocation(1, "hD44wQkJk1har7RM7oznpG4AAABQcm9ncmFtLmNz")] // 2 + Diagnostic(ErrorCode.ERR_InterceptorPositionBadToken, "InterceptsLocation").WithArguments("new").WithLocation(6, 6), + // Interceptors.cs(7,6): error CS9151: Possible method name 'Program' cannot be intercepted because it is not being invoked. + // [InterceptsLocation(1, "hD44wQkJk1har7RM7oznpJQAAABQcm9ncmFtLmNz")] // 3 + Diagnostic(ErrorCode.ERR_InterceptorNameNotInvoked, "InterceptsLocation").WithArguments("Program").WithLocation(7, 6) + ); + } + [Fact] public void InterceptableMethod_BadMethodKind_02() { @@ -1946,6 +2125,51 @@ static class D Diagnostic(ErrorCode.ERR_InterceptorContainingTypeCannotBeGeneric, @"InterceptsLocation(""Program.cs"", 15, 11)").WithArguments("D.Interceptor1(string)").WithLocation(21, 6)); } + [Fact] + public void InterceptorCannotBeGeneric_Checksum_02() + { + var source = CSharpTestSource.Parse(""" + using System; + + interface I1 { } + class C : I1 + { + + public static void InterceptableMethod(string param) { Console.Write("interceptable " + param); } + } + + static class Program + { + public static void Main() + { + C.InterceptableMethod("call site"); + } + } + """, "Program.cs", options: RegularWithInterceptors); + var comp = CreateCompilation(source); + comp.VerifyDiagnostics(); + + var invocation = source.GetRoot().DescendantNodes().OfType().Last(); + var model = comp.GetSemanticModel(source); + var location = model.GetInterceptableLocation(invocation)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System; + + static class D + { + {{location.GetInterceptsLocationAttributeSyntax()}} + public static void Interceptor1(string param) { Console.Write("interceptor " + param); } + } + """, "Interceptors.cs", options: RegularWithInterceptors); + + comp = CreateCompilation([source, interceptors, s_attributesTree]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(5,6): error CS9138: Method 'D.Interceptor1(string)' cannot be used as an interceptor because its containing type has type parameters. + // [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "ZCdvmiprtZ938pueLU5g6OsAAABQcm9ncmFtLmNz")] + Diagnostic(ErrorCode.ERR_InterceptorContainingTypeCannotBeGeneric, "global::System.Runtime.CompilerServices.InterceptsLocationAttribute").WithArguments("D.Interceptor1(string)").WithLocation(5, 6)); + } + [Fact] public void InterceptorCannotBeGeneric_03() { @@ -2547,9 +2771,9 @@ static class D // 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)' + // Program.cs(9,6): error CS1729: 'InterceptsLocationAttribute' does not contain a constructor that takes 0 arguments // [InterceptsLocation()] - Diagnostic(ErrorCode.ERR_NoCorrespondingArgument, "InterceptsLocation()").WithArguments("filePath", "System.Runtime.CompilerServices.InterceptsLocationAttribute.InterceptsLocationAttribute(string, int, int)").WithLocation(9, 6) + Diagnostic(ErrorCode.ERR_BadCtorArgCount, "InterceptsLocation()").WithArguments("System.Runtime.CompilerServices.InterceptsLocationAttribute", "0").WithLocation(9, 6) ); } @@ -3026,6 +3250,108 @@ public static void Main() ); } + [Fact] + public void InterceptsLocationBadPosition_Checksum_01() + { + var sourceTree = CSharpTestSource.Parse(""" + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 { } + + static class Program + { + + 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"); + } + } + """, options: RegularWithInterceptors); + + // test unexpected position within interceptable name token + var interceptableName = sourceTree.GetRoot().DescendantNodes().OfType().Last().GetInterceptableNameSyntax()!; + var position = interceptableName.Position + 1; + + var builder = new BlobBuilder(); + builder.WriteBytes(sourceTree.GetText().GetContentHash()); + builder.WriteInt32(position); + builder.WriteUTF8("Error"); + + var base64 = Convert.ToBase64String(builder.ToArray()); + + var interceptorTree = CSharpTestSource.Parse($$""" + using System.Runtime.CompilerServices; + using System; + + static class D + { + [InterceptsLocation(1, "{{base64}}")] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """, options: RegularWithInterceptors); + var comp = CreateCompilation([sourceTree, interceptorTree, s_attributesTree]); + comp.VerifyEmitDiagnostics( + // (6,6): error CS9235: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. + // [InterceptsLocation(1, "ExWKMussA+NMlN5J0QNXiEMBAABFcnJvcg==")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, "InterceptsLocation").WithArguments("Error").WithLocation(6, 6) + ); + } + + [Theory] + [InlineData(-1)] // test invalid position + [InlineData(99999)] // test position past end of the file + public void InterceptsLocationBadPosition_Checksum_02(int position) + { + var sourceTree = CSharpTestSource.Parse(""" + using System.Runtime.CompilerServices; + using System; + + interface I1 { } + class C : I1 { } + + static class Program + { + + 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"); + } + } + """, options: RegularWithInterceptors); + + var builder = new BlobBuilder(); + builder.WriteBytes(sourceTree.GetText().GetContentHash()); + builder.WriteInt32(position); + builder.WriteUTF8("Error"); + + var base64 = Convert.ToBase64String(builder.ToArray()); + + var interceptorTree = CSharpTestSource.Parse($$""" + using System.Runtime.CompilerServices; + using System; + + static class D + { + [InterceptsLocation(1, "{{base64}}")] + public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } + } + """, options: RegularWithInterceptors); + var comp = CreateCompilation([sourceTree, interceptorTree, s_attributesTree]); + comp.VerifyEmitDiagnostics( + // (6,6): error CS9235: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. + // [InterceptsLocation(1, "ExWKMussA+NMlN5J0QNXiJ+GAQBFcnJvcg==")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, "InterceptsLocation").WithArguments("Error").WithLocation(6, 6) + ); + } + [Fact] public void SignatureMismatch_01() { @@ -5328,6 +5654,45 @@ public static void Interceptor() { } Diagnostic(ErrorCode.ERR_InterceptorCannotUseUnmanagedCallersOnly, @"InterceptsLocation(""Program.cs"", 5, 3)").WithLocation(14, 6)); } + [Fact] + public void InterceptorUnmanagedCallersOnly_Checksum() + { + var source = CSharpTestSource.Parse(""" + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + using System; + + C.Interceptable(); + + class C + { + public static void Interceptable() { } + } + """, "Program.cs", RegularWithInterceptors); + var comp = CreateCompilation(source); + var model = comp.GetSemanticModel(source); + var node = source.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + static class D + { + [InterceptsLocation({{locationSpecifier.Version}}, "{{locationSpecifier.Data}}")] + [UnmanagedCallersOnly] + public static void Interceptor() { } + } + """, "Interceptors.cs", RegularWithInterceptors); + + comp = CreateCompilation([source, interceptors, s_attributesTree, CSharpTestSource.Parse(UnmanagedCallersOnlyAttributeDefinition, "UnmanagedCallersOnlyAttribute.cs", RegularWithInterceptors)]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(6,6): error CS9161: An interceptor cannot be marked with 'UnmanagedCallersOnlyAttribute'. + // [InterceptsLocation(1, "SnNcyOJQR8oIDrJpnwBmCWIAAABQcm9ncmFtLmNz")] + Diagnostic(ErrorCode.ERR_InterceptorCannotUseUnmanagedCallersOnly, "InterceptsLocation").WithLocation(6, 6)); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/70841")] public void InterceptorEnumBaseMethod() { @@ -6422,4 +6787,416 @@ public static class D Assert.Null(model.GetInterceptorMethod(call)); } + + // https://github.com/dotnet/roslyn/issues/72265 + // As part of the work to drop support for file path based interceptors, a significant number of existing tests here will need to be ported to checksum-based. + + [Fact] + public void Checksum_01() + { + var source = CSharpTestSource.Parse(""" + class C + { + static void M() => throw null!; + + static void Main() + { + M(); + } + } + """, "Program.cs", RegularWithInterceptors); + + var comp = CreateCompilation(source); + var model = comp.GetSemanticModel(source); + var node = source.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System; + + static class Interceptors + { + {{locationSpecifier.GetInterceptsLocationAttributeSyntax()}} + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var verifier = CompileAndVerify([source, interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)], expectedOutput: "1"); + verifier.VerifyDiagnostics(); + + // again, but using the accessors for specifically retrieving the individual attribute arguments + interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation({{locationSpecifier!.Version}}, "{{locationSpecifier.Data}}")] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + verifier = CompileAndVerify([source, interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)], expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void Checksum_02() + { + var tree = CSharpTestSource.Parse(""" + class C + { + static void M() => throw null!; + + static void Main() + { + M(); + M(); + } + } + """.NormalizeLineEndings(), "path/to/Program.cs", RegularWithInterceptors); + + var comp = CreateCompilation(tree); + var model = comp.GetSemanticModel(tree); + if (tree.GetRoot().DescendantNodes().OfType().ToList() is not [var node, var otherNode]) + { + throw ExceptionUtilities.Unreachable(); + } + + var locationSpecifier = model.GetInterceptableLocation(node); + Assert.False(locationSpecifier!.Equals(null)); + + // Verify behaviors of the public APIs. + Assert.Equal("path/to/Program.cs(7,9)", locationSpecifier!.GetDisplayLocation()); + Assert.Equal(1, locationSpecifier.Version); + Assert.Equal(locationSpecifier, locationSpecifier); + + Assert.NotSame(locationSpecifier, model.GetInterceptableLocation(node)); + Assert.Equal(locationSpecifier, model.GetInterceptableLocation(node)); + Assert.Equal(locationSpecifier.GetHashCode(), model.GetInterceptableLocation(node)!.GetHashCode()); + + // If Data changes it might be the case that 'SourceText.GetContentHash()' has changed algorithms. + // In this case we need to adjust the SourceMethodSymbolWithAttributes.DecodeInterceptsLocationAttribute impl to remain compatible with v1 and consider introducing a v2 which uses the new content hash algorithm. + AssertEx.Equal("xRCCFCvTOZMORzSr/fZQFlIAAABQcm9ncmFtLmNz", locationSpecifier.Data); + AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "xRCCFCvTOZMORzSr/fZQFlIAAABQcm9ncmFtLmNz")]""", locationSpecifier.GetInterceptsLocationAttributeSyntax()); + + var otherLocation = model.GetInterceptableLocation(otherNode)!; + Assert.NotEqual(locationSpecifier, otherLocation); + // While it is not incorrect for the HashCodes of these instances to be equal, we don't expect it in this case. + Assert.NotEqual(locationSpecifier.GetHashCode(), otherLocation.GetHashCode()); + + Assert.Equal("path/to/Program.cs(8,9)", otherLocation.GetDisplayLocation()); + AssertEx.Equal("xRCCFCvTOZMORzSr/fZQFmAAAABQcm9ncmFtLmNz", otherLocation.Data); + AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "xRCCFCvTOZMORzSr/fZQFmAAAABQcm9ncmFtLmNz")]""", otherLocation.GetInterceptsLocationAttributeSyntax()); + + } + + [Fact] + public void Checksum_03() + { + // Invalid base64 + var interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===")] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(6,6): error CS9231: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); + } + + [Fact] + public void Checksum_04() + { + // Test invalid UTF-8 encoded to base64 + + var builder = new BlobBuilder(); + // all zeros checksum and zero position + builder.WriteBytes(value: 0, byteCount: 20); + + // write invalid utf-8 + builder.WriteByte(0xc0); + + var base64 = Convert.ToBase64String(builder.ToArray()); + + var interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation(1, "{{base64}}")] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(6,6): error CS9231: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, "AAAAAAAAAAAAAAAAAAAAAAAAAADA")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); + } + + [Theory] + [InlineData("")] + [InlineData("AA==")] + public void Checksum_05(string data) + { + // Test data value too small + var interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation(1, "{{data}}")] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(6,6): error CS9231: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, "")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); + } + + [Fact] + public void Checksum_06() + { + // Null data + var interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation(1, null)] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(6,6): error CS9231: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, null)] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); + } + + [Fact] + public void Checksum_07() + { + // File not found + + var source = CSharpTestSource.Parse(""" + class C + { + static void M() => throw null!; + + static void Main() + { + M(); + } + } + """, "Program.cs", RegularWithInterceptors); + + var comp = CreateCompilation(source); + var model = comp.GetSemanticModel(source); + var node = source.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation({{locationSpecifier.Version}}, "{{locationSpecifier.Data}}")] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var comp1 = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp1.VerifyEmitDiagnostics( + // Interceptors.cs(6,6): error CS9234: Cannot intercept a call in file 'Program.cs' because a matching file was not found in the compilation. + // [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6FIAAABQcm9ncmFtLmNz")] + Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, "InterceptsLocation").WithArguments("Program.cs").WithLocation(6, 6)); + } + + [Fact] + public void Checksum_08() + { + // Duplicate file + + var source = """ + class C + { + static void M() => throw null!; + + static void Main() + { + M(); + } + } + """; + var sourceTree1 = CSharpTestSource.Parse(source, path: "Program1.cs", options: RegularWithInterceptors); + + var comp = CreateCompilation(sourceTree1); + var model = comp.GetSemanticModel(sourceTree1); + var node = sourceTree1.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation({{locationSpecifier.Version}}, "{{locationSpecifier.Data}}")] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var comp1 = CreateCompilation([ + sourceTree1, + CSharpTestSource.Parse(source, path: "Program2.cs", options: RegularWithInterceptors), + interceptors, + CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp1.GetDiagnostics().Where(d => d.Location.SourceTree == interceptors).Verify( + // Interceptors.cs(6,6): error CS9233: Cannot intercept a call in file 'Program1.cs' because it is duplicated elsewhere in the compilation. + // [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6FIAAABQcm9ncmFtMS5jcw==")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDuplicateFile, "InterceptsLocation").WithArguments("Program1.cs").WithLocation(6, 6)); + } + + [Fact] + public void Checksum_09() + { + // Call can be intercepted syntactically but a semantic error occurs when actually performing it. + + var source = CSharpTestSource.Parse(""" + using System; + + class C + { + static Action P { get; } = null!; + + static void Main() + { + P(); + } + } + """, "Program.cs", RegularWithInterceptors); + + var comp = CreateCompilation(source); + var model = comp.GetSemanticModel(source); + var node = source.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node)!; + + var interceptors = CSharpTestSource.Parse($$""" + using System; + + static class Interceptors + { + {{locationSpecifier.GetInterceptsLocationAttributeSyntax()}} + public static void P1(this C c) => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + comp = CreateCompilation([source, interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(5,6): error CS9207: Cannot intercept 'P' because it is not an invocation of an ordinary member method. + // [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "ZnP1PXDK5WDD07FTErR9eWUAAABQcm9ncmFtLmNz")] + Diagnostic(ErrorCode.ERR_InterceptableMethodMustBeOrdinary, "global::System.Runtime.CompilerServices.InterceptsLocationAttribute").WithArguments("P").WithLocation(5, 6)); + } + + [Fact] + public void Checksum_10() + { + // Call cannot be intercepted syntactically + + var source = CSharpTestSource.Parse(""" + using System; + + static class C + { + public static void M(this object obj) => throw null!; + + static void Main() + { + null(); + } + } + """, "Program.cs", RegularWithInterceptors); + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // Program.cs(9,9): error CS0149: Method name expected + // null(); + Diagnostic(ErrorCode.ERR_MethodNameExpected, "null").WithLocation(9, 9)); + + var model = comp.GetSemanticModel(source); + var node = source.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node); + Assert.Null(locationSpecifier); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(2)] + [InlineData(9999)] + public void Checksum_11(int version) + { + // Bad version + var interceptors = CSharpTestSource.Parse($$""" + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation({{version}}, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===")] + public static void M1() => Console.Write(1); + } + """, "Interceptors.cs", RegularWithInterceptors); + + var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); + comp.VerifyEmitDiagnostics( + // Interceptors.cs(6,6): error CS9232: Version '0' of the interceptors format is not supported. The latest supported version is '1'. + // [InterceptsLocation(0, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===")] + Diagnostic(ErrorCode.ERR_InterceptsLocationUnsupportedVersion, "InterceptsLocation").WithArguments($"{version}").WithLocation(6, 6)); + } + + [Fact] + public void Checksum_12() + { + // Attempt to insert null paths into InterceptableLocation. + + var tree = CSharpTestSource.Parse(""" + class C + { + static void M() => throw null!; + + static void Main() + { + M(); + } + } + """.NormalizeLineEndings(), path: null, RegularWithInterceptors); + Assert.Equal("", tree.FilePath); + + var comp = CreateCompilation(tree); + var model = comp.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node)!; + Assert.Equal("(7,9)", locationSpecifier.GetDisplayLocation()); + AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "jB4qgCy292LkEGCwmD+R6FIAAAA=")]""", locationSpecifier.GetInterceptsLocationAttributeSyntax()); + } } diff --git a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs index 47dfa645cde10..5bc7afbc0cc23 100644 --- a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs @@ -4080,5 +4080,101 @@ internal static void VerifyGeneratorExceptionDiagnostic( var expectedDetails = $"System.{typeName}: {message}{Environment.NewLine} "; Assert.StartsWith(expectedDetails, diagnostic.Arguments[3] as string); } + + [Fact] + public void GetInterceptsLocationSpecifier_01() + { + var generator = new IncrementalGeneratorWrapper(new InterceptorGenerator1()); + + var parseOptions = TestOptions.RegularPreview.WithFeature("InterceptorsPreviewNamespaces", "global"); + + var source1 = (""" + public class Program + { + public static void Main() + { + var program = new Program(); + program.M(1); + } + + public void M(int param) => throw null!; + } + + namespace System.Runtime.CompilerServices + { + public class InterceptsLocationAttribute : Attribute { public InterceptsLocationAttribute(int version, string data) { } } + } + """, PlatformInformation.IsWindows ? @"C:\project\src\Program.cs" : "/project/src/Program.cs"); + + Compilation compilation = CreateCompilation([source1], options: TestOptions.DebugExe, parseOptions: parseOptions); + + GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions() { BaseDirectory = PlatformInformation.IsWindows ? @"C:\project\obj\" : "/project/obj" }); + verify(ref driver, compilation); + + void verify(ref GeneratorDriver driver, Compilation compilation) + { + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); + outputCompilation.VerifyDiagnostics(); + CompileAndVerify(outputCompilation, expectedOutput: "1"); + generatorDiagnostics.Verify(); + } + } + + [Generator(LanguageNames.CSharp)] + private class InterceptorGenerator1 : IIncrementalGenerator + { +#pragma warning disable RSEXPERIMENTAL002 // test + record InterceptorInfo(InterceptableLocation locationSpecifier, object data); + + private static bool IsInterceptableCall(SyntaxNode node, CancellationToken token) => node is InvocationExpressionSyntax; + + private static object GetData(GeneratorSyntaxContext context) => 1; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var interceptorInfos = context.SyntaxProvider.CreateSyntaxProvider( + predicate: IsInterceptableCall, + transform: (GeneratorSyntaxContext context, CancellationToken token) => + { + var model = context.SemanticModel; + var locationSpecifier = model.GetInterceptableLocation((InvocationExpressionSyntax)context.Node, token); + if (locationSpecifier is null) + { + return null; // generator wants to intercept call, but host thinks call is not interceptable. bug. + } + + // generator is careful to propagate only equatable data (i.e., not syntax nodes or symbols). + return new InterceptorInfo(locationSpecifier, GetData(context)); + }) + .Where(info => info != null) + .Collect(); + + context.RegisterSourceOutput(interceptorInfos, (context, interceptorInfos) => + { + var builder = new StringBuilder(); + builder.AppendLine("using System.Runtime.CompilerServices;"); + builder.AppendLine("using System;"); + builder.AppendLine("public static class Interceptors"); + builder.AppendLine("{"); + // builder boilerplate.. + foreach (var interceptorInfo in interceptorInfos) + { + var (locationSpecifier, data) = interceptorInfo!; + builder.AppendLine($$""" + // {{locationSpecifier.GetDisplayLocation()}} + [InterceptsLocation({{locationSpecifier.Version}}, "{{locationSpecifier.Data}}")] + public static void Interceptor(this Program program, int param) + { + Console.Write(1); + } + """); + } + // builder boilerplate.. + builder.AppendLine("}"); + + context.AddSource("MyInterceptors.cs", builder.ToString()); + }); + } + } } } diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs index dab79d5a56a83..cf440cccf3c85 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs @@ -181,6 +181,7 @@ static AttributeDescription() private static readonly byte[] s_signature_HasThis_Void_Type_String = new byte[] { (byte)SignatureAttributes.Instance, 2, Void, TypeHandle, (byte)TypeHandleTarget.SystemType, String }; 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_Int32_String = new byte[] { (byte)SignatureAttributes.Instance, 2, Void, Int32, String }; 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 }; @@ -225,7 +226,7 @@ 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_signaturesOfInterceptsLocationAttribute = { s_signature_HasThis_Void_String_Int32_Int32 }; + private static readonly byte[][] s_signaturesOfInterceptsLocationAttribute = { s_signature_HasThis_Void_String_Int32_Int32, s_signature_HasThis_Void_Int32_String }; private static readonly byte[][] s_signaturesOfPrincipalPermissionAttribute = { s_signature_HasThis_Void_SecurityAction }; private static readonly byte[][] s_signaturesOfPermissionSetAttribute = { s_signature_HasThis_Void_SecurityAction };