From 3f1a038cfee3cb42a648837de8b74a1146424968 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Fri, 29 Mar 2024 17:52:29 -0700 Subject: [PATCH 01/22] Add support for checksum-based interceptors --- docs/features/interceptors.md | 46 +-- .../CSharp/Portable/CSharpExtensions.cs | 20 ++ .../CSharp/Portable/CSharpResources.resx | 12 + .../Portable/Compilation/CSharpCompilation.cs | 43 +++ .../Compilation/CSharpSemanticModel.cs | 21 ++ .../CSharp/Portable/Errors/ErrorCode.cs | 5 + .../CSharp/Portable/Errors/ErrorFacts.cs | 4 + .../CSharp/Portable/PublicAPI.Unshipped.txt | 6 + .../SourceMethodSymbolWithAttributes.cs | 246 +++++++++++++++- .../Utilities/InterceptableLocation.cs | 105 +++++++ .../Portable/xlf/CSharpResources.cs.xlf | 20 ++ .../Portable/xlf/CSharpResources.de.xlf | 20 ++ .../Portable/xlf/CSharpResources.es.xlf | 20 ++ .../Portable/xlf/CSharpResources.fr.xlf | 20 ++ .../Portable/xlf/CSharpResources.it.xlf | 20 ++ .../Portable/xlf/CSharpResources.ja.xlf | 20 ++ .../Portable/xlf/CSharpResources.ko.xlf | 20 ++ .../Portable/xlf/CSharpResources.pl.xlf | 20 ++ .../Portable/xlf/CSharpResources.pt-BR.xlf | 20 ++ .../Portable/xlf/CSharpResources.ru.xlf | 20 ++ .../Portable/xlf/CSharpResources.tr.xlf | 20 ++ .../Portable/xlf/CSharpResources.zh-Hans.xlf | 20 ++ .../Portable/xlf/CSharpResources.zh-Hant.xlf | 20 ++ .../Semantic/Semantics/InterceptorsTests.cs | 277 +++++++++++++++++- .../SourceGeneration/GeneratorDriverTests.cs | 96 ++++++ .../Attributes/AttributeDescription.cs | 3 +- 26 files changed, 1106 insertions(+), 38 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index b96fe71e646b6..32eba1332c0ff 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,29 @@ 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 one-based line number of the call in syntax. +- int32 in little-endian format for the one-based column number 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`. +Line and column numbers in the "version 1" data encoding 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)` 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 +103,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 +115,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 +136,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..1c83da09a88fe 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 'node' 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) + { + var csModel = semanticModel as CSharpSemanticModel; + return csModel?.GetInterceptableLocation(node); + } + + /// + /// 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 4118183fc2315..98c25bbc9b272 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7905,4 +7905,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Modifiers cannot be placed on using declarations + + 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. + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index f71459b318246..fa1dbde8d323b 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -160,8 +160,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 +1079,45 @@ ImmutableSegmentedDictionary> computeMappedPathToS } } + private class ContentHashComparer : IEqualityComparer> + { + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + { + return x.Span.SequenceEqual(y.Span); + } + + public int GetHashCode(ReadOnlyMemory obj) + { + return Hash.GetFNVHashCode(obj.Span, out _); + } + + public static ContentHashComparer Instance { get; } = new ContentHashComparer(); + } + + internal OneOrMany GetSyntaxTreesByContentHash(ReadOnlyMemory contentHash) + { + 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.ContainsKey(hash) ? builder[hash].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. diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs index df844fd007afa..7978acd185eaf 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs @@ -5214,6 +5214,27 @@ protected sealed override ISymbol GetDeclaredSymbolCore(SyntaxNode node, Cancell return null; } + +#pragma warning disable RSEXPERIMENTAL002 // Internal usage of experimental API + public InterceptableLocation? GetInterceptableLocation(InvocationExpressionSyntax node) + { + CheckSyntaxNode(node); + if (node.GetInterceptableNameSyntax() is not { } nameSyntax) + { + return null; + } + + var tree = node.SyntaxTree; + var text = tree.GetText(); + 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, 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 70c3ed6d8b861..13fd72ebe09de 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2302,6 +2302,11 @@ internal enum ErrorCode ERR_NoModifiersOnUsing = 9229, + ERR_InterceptsLocationDataInvalidFormat = 9230, + ERR_InterceptsLocationUnsupportedVersion = 9231, + ERR_InterceptsLocationDuplicateFile = 9232, + ERR_InterceptsLocationFileNotFound = 9233, + #endregion // Note: you will need to do the following after adding warnings: diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index b4eaef25f8e7a..a590e7fae9f0a 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -2431,6 +2431,10 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_ParamsCollectionExtensionAddMethod: case ErrorCode.ERR_ParamsCollectionMissingConstructor: case ErrorCode.ERR_NoModifiersOnUsing: + case ErrorCode.ERR_InterceptsLocationDataInvalidFormat: + case ErrorCode.ERR_InterceptsLocationUnsupportedVersion: + case ErrorCode.ERR_InterceptsLocationDuplicateFile: + case ErrorCode.ERR_InterceptsLocationFileNotFound: return false; default: // NOTE: All error codes must be explicitly handled in this switch statement diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index a7eede3cf18d3..70b883244e9d8 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! @@ -18,3 +22,5 @@ static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.CreateTokenParser(Microsoft.C 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]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) -> 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..c2b381c11de9c 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,25 +944,249 @@ 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; + if (version != 1) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationUnsupportedVersion, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 0)); + return; + } + + if (data is null) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + return; + } + + byte[] bytes; + try + { + bytes = Convert.FromBase64String(data); + } + catch (FormatException) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + return; + } + + // format: + // - 16 bytes of hash (xxHash128) + // - int32 line number (little endian) + // - int32 character number (little endian) + // - utf-8 display filename + const int minLength = 16 + 4 + 4; + if (bytes.Length < minLength) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + return; + } + + var hash = bytes.AsMemory(start: 0, length: 16); + var lineNumberOneBased = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: 16)); + var characterNumberOneBased = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: 20)); + + string displayFileName; + try + { + displayFileName = Encoding.UTF8.GetString(bytes, index: 24, count: bytes.Length - minLength); + } + catch (ArgumentException) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + return; + } + + var attributeSyntax = arguments.AttributeSyntaxOpt; + Debug.Assert(attributeSyntax is object); + var attributeLocation = attributeSyntax.Location; + + var interceptorsNamespaces = ((CSharpParseOptions)attributeSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; + var thisNamespaceNames = getNamespaceNames(); + var foundAnyMatch = interceptorsNamespaces.Any(ns => isDeclaredInNamespace(thisNamespaceNames, ns)); + if (!foundAnyMatch) + { + reportFeatureNotEnabled(diagnostics, attributeSyntax, thisNamespaceNames); + thisNamespaceNames.Free(); + return; + } + thisNamespaceNames.Free(); + + var attributeData = arguments.Attribute; + + 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, attributeData.GetAttributeArgumentLocation(1), displayFileName); + return; + } + + if (matchingTrees.Count == 0) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationFileNotFound, attributeData.GetAttributeArgumentLocation(1), displayFileName); + return; + } + + Debug.Assert(matchingTrees.Count == 1); + SyntaxTree? matchingTree = matchingTrees[0]; + // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. + int lineNumberZeroBased = lineNumberOneBased - 1; + int characterNumberZeroBased = characterNumberOneBased - 1; + + if (lineNumberZeroBased < 0 || characterNumberZeroBased < 0) + { + var location = attributeData.GetAttributeArgumentLocation(1); + diagnostics.Add(ErrorCode.ERR_InterceptorLineCharacterMustBePositive, location); + return; + } + + var referencedLines = matchingTree.GetText().Lines; + var referencedLineCount = referencedLines.Count; + + if (lineNumberZeroBased >= referencedLineCount) + { + diagnostics.Add(ErrorCode.ERR_InterceptorLineOutOfRange, attributeData.GetAttributeArgumentLocation(1), referencedLineCount, lineNumberOneBased); + return; + } + + var line = referencedLines[lineNumberZeroBased]; + var lineLength = line.End - line.Start; + if (characterNumberZeroBased >= lineLength) + { + diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeData.GetAttributeArgumentLocation(1), lineLength, characterNumberOneBased); + return; + } + + var referencedPosition = line.Start + characterNumberZeroBased; + var root = matchingTree.GetRoot(); + var referencedToken = root.FindToken(referencedPosition); + switch (referencedToken) + { + case { Parent: SimpleNameSyntax { Parent: MemberAccessExpressionSyntax { Parent: InvocationExpressionSyntax } memberAccess } rhs } when memberAccess.Name == rhs: + case { Parent: SimpleNameSyntax { Parent: InvocationExpressionSyntax invocation } simpleName } when invocation.Expression == simpleName: + // happy case + break; + 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; + } + + // Did they actually refer to the start of the token, not the middle, or in trivia? + if (referencedPosition != referencedToken.Span.Start) + { + 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); + + // Caller must free the returned builder. + ArrayBuilder getNamespaceNames() + { + var namespaceNames = ArrayBuilder.GetInstance(); + for (var containingNamespace = 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, AttributeSyntax attributeSyntax, ArrayBuilder namespaceNames) + { + if (namespaceNames.Count == 0) + { + diagnostics.Add(ErrorCode.ERR_InterceptorGlobalNamespace, attributeSyntax); + } + else + { + var recommendedProperty = $"$(InterceptorsPreviewNamespaces);{string.Join(".", namespaceNames)}"; + diagnostics.Add(ErrorCode.ERR_InterceptorsFeatureNotEnabled, attributeSyntax, 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; - const int characterNumberParameterIndex = 2; + int filePathParameterIndex = 0; + int lineNumberParameterIndex = 1; + int characterNumberParameterIndex = 2; var interceptorsNamespaces = ((CSharpParseOptions)attributeSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; var thisNamespaceNames = getNamespaceNames(); @@ -973,7 +1199,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)); diff --git a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs new file mode 100644 index 0000000000000..7c52110d29ca3 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +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; } + + public abstract string GetDisplayLocation(); +} + +#pragma warning disable RSEXPERIMENTAL002 // internal usage of experimental API +/// +/// Version 1 of the InterceptableLocation encoding. +/// +internal sealed class InterceptableLocation1 : InterceptableLocation +{ + private readonly ImmutableArray _checksum; + private readonly string _path; + private readonly int _lineNumberOneIndexed; + private readonly int _characterNumberOneIndexed; + + internal InterceptableLocation1(ImmutableArray checksum, string path, int lineNumberOneIndexed, int characterNumberOneIndexed) + { + _checksum = checksum; + _path = path; + _lineNumberOneIndexed = lineNumberOneIndexed; + _characterNumberOneIndexed = characterNumberOneIndexed; + } + + public override string GetDisplayLocation() + { + // e.g. `C:\project\src\Program.cs(12,34)` + return $"{_path}({_lineNumberOneIndexed},{_characterNumberOneIndexed})"; + } + + public override int Version => 1; + public override string Data + { + get + { + if (_checksum.Length != 16) + { + throw new InvalidOperationException(); + } + + var builder = new BlobBuilder(); + builder.WriteBytes(_checksum, start: 0, 16); + builder.WriteInt32(_lineNumberOneIndexed); + builder.WriteInt32(_characterNumberOneIndexed); + + var displayFileName = Path.GetFileName(_path); + builder.WriteUTF8(displayFileName); + + return Convert.ToBase64String(builder.ToArray()); + } + } + + // 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 + && _lineNumberOneIndexed == other._lineNumberOneIndexed + && _characterNumberOneIndexed == other._characterNumberOneIndexed; + } + + public override int GetHashCode() + { + return Hash.Combine( + Hash.GetFNVHashCode(_checksum), + Hash.Combine( + _path.GetHashCode(), + Hash.Combine( + _lineNumberOneIndexed, + _characterNumberOneIndexed))); + } +} diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index bb92a4ac76dff..8f85c2ba46878 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -1097,6 +1097,26 @@ 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. + + + + 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 402eceae91027..35ee232a35db3 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -1097,6 +1097,26 @@ 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. + + + + 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 ba86940e21765..27c622cd7e2bf 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -1097,6 +1097,26 @@ 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. + + + + 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 211db1f05ced1..33dc640da4424 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -1097,6 +1097,26 @@ 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. + + + + 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 c04e9acee1b5b..b399c37dbae65 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -1097,6 +1097,26 @@ 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. + + + + 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 c2b88fa3fce5c..8aefad038d9cc 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -1097,6 +1097,26 @@ 'インターセプター' の実験的な機能は、この名前空間では有効になっていません。プロジェクトに '{0}' を追加します。 + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + 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 b629fbf9669db..37f071a2984af 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -1097,6 +1097,26 @@ 이 네임스페이스에서는 '인터셉터' 실험적 기능을 사용할 수 없습니다. 프로젝트에 '{0}'을(를) 추가하세요. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + 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 6ba13a221fcd5..5d30ae37fac56 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -1097,6 +1097,26 @@ 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. + + + + 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 4835a1d3b3cd4..270425b488c2c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -1097,6 +1097,26 @@ 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. + + + + 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 1a0be41c5094d..2f44b3f3b34e5 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -1097,6 +1097,26 @@ Экспериментальная функция "перехватчики" не включена в этом пространстве имен. Добавьте "{0}" в свой проект. + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + 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 f89a48cf7ddf0..c0cf596434f4d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -1097,6 +1097,26 @@ '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. + + + + 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 d50172d420482..3602114fd0cd2 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -1097,6 +1097,26 @@ 此命名空间中未启用“拦截器”实验性功能。请将“{0}”添加到项目。 + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + 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 6b46c6052739f..c2b50a054d740 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -1097,6 +1097,26 @@ 未在此命名空間中啟用「攔截器」實驗功能。將 '{0}' 新增至您的專案。 + + The data argument to InterceptsLocationAttribute is not in the correct format. + The data argument to InterceptsLocationAttribute is not in the correct format. + + + + 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..4c81e220d21be 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,15 +22,14 @@ 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"); @@ -6422,4 +6422,273 @@ public static class D Assert.Null(model.GetInterceptorMethod(call)); } + + [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(); + } + } + """.NormalizeLineEndings(), "path/to/Program.cs", RegularWithInterceptors); + + var comp = CreateCompilation(tree); + var model = comp.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + var locationSpecifier = model.GetInterceptableLocation(node)!; + + // 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("jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==", locationSpecifier.Data); + AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==")]""", locationSpecifier.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,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, @"""jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===""").WithLocation(6, 28)); + } + + [Fact] + public void Checksum_04() + { + // Test invalid UTF-8 encoded to base64 + + var builder = new BlobBuilder(); + // all zeros checksum and zero line and column numbers (which are also invalid, but won't be foudn with this particular test) + builder.WriteBytes(value: 0, byteCount: 12); + + // 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,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, "AAAAAAAAAAAAAAAAwA==")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, @"""AAAAAAAAAAAAAAAAwA==""").WithLocation(6, 28)); + } + + [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,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, "{data}")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, $@"""{data}""").WithLocation(6, 28)); + } + + [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,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, null)] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "null").WithLocation(6, 28)); + } + + [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,28): error CS9232: Cannot intercept a call in file 'Program.cs' because a matching file was not found in the compilation. + // [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==")] + Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, @"""jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==""").WithArguments("Program.cs").WithLocation(6, 28)); + } + + [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,28): error CS9231: Cannot intercept a call in file 'Program1.cs' because it is duplicated elsewhere in the compilation. + // [InterceptsLocation(1, "{data}")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDuplicateFile, $@"""{locationSpecifier.Data}""").WithArguments("Program1.cs").WithLocation(6, 28)); + } } diff --git a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs index 47dfa645cde10..70c83d56005e5 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); + 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 }; From 53e0548b9484fec9c1e640b314a67bb3d48ebbc2 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 11:27:55 -0700 Subject: [PATCH 02/22] Fix broken tests --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 4c81e220d21be..1e533d6798e8f 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -2547,9 +2547,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) ); } @@ -6643,8 +6643,8 @@ static class Interceptors var comp1 = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp1.VerifyEmitDiagnostics( // Interceptors.cs(6,28): error CS9232: Cannot intercept a call in file 'Program.cs' because a matching file was not found in the compilation. - // [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==")] - Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, @"""jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==""").WithArguments("Program.cs").WithLocation(6, 28)); + // [InterceptsLocation(1, "{locationSpecifier.Data}")] + Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, $@"""{locationSpecifier.Data}""").WithArguments("Program.cs").WithLocation(6, 28)); } [Fact] From f12ce017061380b7d2b3d0ceeca3fe04300b63d9 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 14:19:05 -0700 Subject: [PATCH 03/22] use position --- docs/features/interceptors.md | 3 +- .../CSharp/Portable/CSharpResources.resx | 3 + .../Portable/Compilation/CSharpCompilation.cs | 19 ++- .../Compilation/CSharpSemanticModel.cs | 4 +- .../CSharp/Portable/Errors/ErrorCode.cs | 1 + .../CSharp/Portable/Errors/ErrorFacts.cs | 1 + .../Lowering/LocalRewriter/LocalRewriter.cs | 2 +- .../LocalRewriter/LocalRewriter_Call.cs | 4 +- ...LocalRewriter_FunctionPointerInvocation.cs | 2 +- .../SourceMethodSymbolWithAttributes.cs | 61 ++++----- .../Utilities/InterceptableLocation.cs | 17 ++- .../Portable/xlf/CSharpResources.cs.xlf | 5 + .../Portable/xlf/CSharpResources.de.xlf | 5 + .../Portable/xlf/CSharpResources.es.xlf | 5 + .../Portable/xlf/CSharpResources.fr.xlf | 5 + .../Portable/xlf/CSharpResources.it.xlf | 5 + .../Portable/xlf/CSharpResources.ja.xlf | 5 + .../Portable/xlf/CSharpResources.ko.xlf | 5 + .../Portable/xlf/CSharpResources.pl.xlf | 5 + .../Portable/xlf/CSharpResources.pt-BR.xlf | 5 + .../Portable/xlf/CSharpResources.ru.xlf | 5 + .../Portable/xlf/CSharpResources.tr.xlf | 5 + .../Portable/xlf/CSharpResources.zh-Hans.xlf | 5 + .../Portable/xlf/CSharpResources.zh-Hant.xlf | 5 + .../Semantic/Semantics/InterceptorsTests.cs | 117 +++++++++++++++++- 25 files changed, 232 insertions(+), 67 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 32eba1332c0ff..1b9b816eb3ab5 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -76,8 +76,7 @@ The arguments to `[InterceptsLocation]` are: 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 one-based line number of the call in syntax. -- int32 in little-endian format for the one-based column number of the call in syntax. +- 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 diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 98c25bbc9b272..195fa96b72215 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7917,4 +7917,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ 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}'. + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index fa1dbde8d323b..f9038d840b67f 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; @@ -1088,7 +1089,7 @@ public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) public int GetHashCode(ReadOnlyMemory obj) { - return Hash.GetFNVHashCode(obj.Span, out _); + return BinaryPrimitives.ReadInt32LittleEndian(obj.Span); } public static ContentHashComparer Instance { get; } = new ContentHashComparer(); @@ -1112,7 +1113,7 @@ ImmutableSegmentedDictionary, OneOrMany> comput { var text = tree.GetText(); var hash = text.GetContentHash().AsMemory(); - builder[hash] = builder.ContainsKey(hash) ? builder[hash].Add(tree) : OneOrMany.Create(tree); + builder[hash] = builder.TryGetValue(hash, out var existing) ? existing.Add(tree) : OneOrMany.Create(tree); } return builder.ToImmutable(); } @@ -2442,15 +2443,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) => { @@ -2470,9 +2471,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; } @@ -2483,9 +2484,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 7978acd185eaf..af022bfeba3ed 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs @@ -5207,7 +5207,7 @@ 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(); } @@ -5233,7 +5233,7 @@ protected sealed override ISymbol GetDeclaredSymbolCore(SyntaxNode node, Cancell var lineNumberOneIndexed = lineSpan.Line + 1; var characterNumberOneIndexed = lineSpan.Character + 1; - return new InterceptableLocation1(checksum, path, lineNumberOneIndexed, characterNumberOneIndexed); + return new InterceptableLocation1(checksum, path, nameSyntax.Position, lineNumberOneIndexed, characterNumberOneIndexed); } #nullable disable diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 13fd72ebe09de..6fc2ca688d676 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2306,6 +2306,7 @@ internal enum ErrorCode ERR_InterceptsLocationUnsupportedVersion = 9231, ERR_InterceptsLocationDuplicateFile = 9232, ERR_InterceptsLocationFileNotFound = 9233, + ERR_InterceptsLocationDataInvalidPosition = 9234, #endregion diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index a590e7fae9f0a..34ebbe0060d88 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -2435,6 +2435,7 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) 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 7e7c8d253ac70..9fd8edb3d325d 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/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index c2b381c11de9c..c4be2578985c8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -987,25 +987,29 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum } // format: - // - 16 bytes of hash (xxHash128) - // - int32 line number (little endian) - // - int32 character number (little endian) + // - 16 bytes of target file content hash (xxHash128) + // - int32 position (little endian) // - utf-8 display filename - const int minLength = 16 + 4 + 4; + 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) { diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); return; } - var hash = bytes.AsMemory(start: 0, length: 16); - var lineNumberOneBased = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: 16)); - var characterNumberOneBased = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: 20)); + var hash = bytes.AsMemory(start: hashIndex, length: hashSize); + var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: positionIndex)); string displayFileName; try { - displayFileName = Encoding.UTF8.GetString(bytes, index: 24, count: bytes.Length - minLength); + displayFileName = Encoding.UTF8.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); } catch (ArgumentException) { @@ -1065,37 +1069,17 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum Debug.Assert(matchingTrees.Count == 1); SyntaxTree? matchingTree = matchingTrees[0]; - // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. - int lineNumberZeroBased = lineNumberOneBased - 1; - int characterNumberZeroBased = characterNumberOneBased - 1; - if (lineNumberZeroBased < 0 || characterNumberZeroBased < 0) + var root = matchingTree.GetRoot(); + if (position > root.EndPosition) { - var location = attributeData.GetAttributeArgumentLocation(1); - diagnostics.Add(ErrorCode.ERR_InterceptorLineCharacterMustBePositive, location); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, attributeLocation, displayFileName); return; } var referencedLines = matchingTree.GetText().Lines; var referencedLineCount = referencedLines.Count; - - if (lineNumberZeroBased >= referencedLineCount) - { - diagnostics.Add(ErrorCode.ERR_InterceptorLineOutOfRange, attributeData.GetAttributeArgumentLocation(1), referencedLineCount, lineNumberOneBased); - return; - } - - var line = referencedLines[lineNumberZeroBased]; - var lineLength = line.End - line.Start; - if (characterNumberZeroBased >= lineLength) - { - diagnostics.Add(ErrorCode.ERR_InterceptorCharacterOutOfRange, attributeData.GetAttributeArgumentLocation(1), lineLength, characterNumberOneBased); - return; - } - - var referencedPosition = line.Start + characterNumberZeroBased; - var root = matchingTree.GetRoot(); - var referencedToken = root.FindToken(referencedPosition); + var referencedToken = root.FindToken(position); switch (referencedToken) { case { Parent: SimpleNameSyntax { Parent: MemberAccessExpressionSyntax { Parent: InvocationExpressionSyntax } memberAccess } rhs } when memberAccess.Name == rhs: @@ -1113,15 +1097,13 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum return; } - // Did they actually refer to the start of the token, not the middle, or in trivia? - if (referencedPosition != referencedToken.Span.Start) + if (position != referencedToken.Position) { - var linePositionZeroBased = referencedToken.GetLocation().GetLineSpan().StartLinePosition; - diagnostics.Add(ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition, attributeLocation, referencedToken.Text, linePositionZeroBased.Line + 1, linePositionZeroBased.Character + 1); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, attributeLocation, displayFileName); return; } - DeclaringCompilation.AddInterception(matchingTree.FilePath, lineNumberZeroBased, characterNumberZeroBased, attributeLocation, this); + DeclaringCompilation.AddInterception(matchingTree.FilePath, position, attributeLocation, this); // Caller must free the returned builder. ArrayBuilder getNamespaceNames() @@ -1326,14 +1308,15 @@ private void DecodeInterceptsLocationAttributeExperimentalCompat( } // 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/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs index 7c52110d29ca3..b187d33ed967e 100644 --- a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -28,6 +28,9 @@ private protected InterceptableLocation() { } /// 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(); } @@ -39,13 +42,15 @@ internal sealed class InterceptableLocation1 : InterceptableLocation { private readonly ImmutableArray _checksum; private readonly string _path; + private readonly int _position; private readonly int _lineNumberOneIndexed; private readonly int _characterNumberOneIndexed; - internal InterceptableLocation1(ImmutableArray checksum, string path, int lineNumberOneIndexed, int characterNumberOneIndexed) + internal InterceptableLocation1(ImmutableArray checksum, string path, int position, int lineNumberOneIndexed, int characterNumberOneIndexed) { _checksum = checksum; _path = path; + _position = position; _lineNumberOneIndexed = lineNumberOneIndexed; _characterNumberOneIndexed = characterNumberOneIndexed; } @@ -68,8 +73,7 @@ public override string Data var builder = new BlobBuilder(); builder.WriteBytes(_checksum, start: 0, 16); - builder.WriteInt32(_lineNumberOneIndexed); - builder.WriteInt32(_characterNumberOneIndexed); + builder.WriteInt32(_position); var displayFileName = Path.GetFileName(_path); builder.WriteUTF8(displayFileName); @@ -88,6 +92,7 @@ public override bool Equals(object? obj) return obj is InterceptableLocation1 other && _checksum.SequenceEqual(other._checksum) && _path == other._path + && _position == other._position && _lineNumberOneIndexed == other._lineNumberOneIndexed && _characterNumberOneIndexed == other._characterNumberOneIndexed; } @@ -99,7 +104,9 @@ public override int GetHashCode() Hash.Combine( _path.GetHashCode(), Hash.Combine( - _lineNumberOneIndexed, - _characterNumberOneIndexed))); + _position, + Hash.Combine( + _lineNumberOneIndexed, + _characterNumberOneIndexed)))); } } diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 8f85c2ba46878..0767b698f006d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 35ee232a35db3..6356882d5f904 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 27c622cd7e2bf..1bf7bf15facb0 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 33dc640da4424..2f77f5558b0f6 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index b399c37dbae65..1ee070701ddf1 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index 8aefad038d9cc..05e98566ae142 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index 37f071a2984af..b458bbbf21428 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index 5d30ae37fac56..39a0e289d3a7d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 270425b488c2c..df7fca6027e8f 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index 2f44b3f3b34e5..e657e95d3058d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index c0cf596434f4d..5f28fc70ac2ca 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 3602114fd0cd2..e4d56e8bc4d3d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index c2b50a054d740..ded68e3b324a8 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -1102,6 +1102,11 @@ 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. diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 1e533d6798e8f..247e1c3cdd27a 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.Linq.Expressions; using System.Reflection.Metadata; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -35,6 +36,8 @@ public InterceptsLocationAttribute(int version, string data) { } 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() { @@ -3026,6 +3029,109 @@ 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 CS9234: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. + // [InterceptsLocation(1, "{base64}")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, $@"InterceptsLocation(1, ""{base64}"")").WithArguments("Error").WithLocation(6, 6) + ); + } + + [Fact] + public void InterceptsLocationBadPosition_Checksum_02() + { + 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 position past end of the file + var position = 99999; + + 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 CS9234: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. + // [InterceptsLocation(1, "{base64}")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, $@"InterceptsLocation(1, ""{base64}"")").WithArguments("Error").WithLocation(6, 6) + ); + } + [Fact] public void SignatureMismatch_01() { @@ -6423,6 +6529,9 @@ 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() { @@ -6503,8 +6612,8 @@ static void Main() // 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("jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==", locationSpecifier.Data); - AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw==")]""", locationSpecifier.GetInterceptsLocationAttributeSyntax()); + AssertEx.Equal("jB4qgCy292LkEGCwmD+R6FIAAABQcm9ncmFtLmNz", locationSpecifier.Data); + AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "jB4qgCy292LkEGCwmD+R6FIAAABQcm9ncmFtLmNz")]""", locationSpecifier.GetInterceptsLocationAttributeSyntax()); } [Fact] @@ -6535,8 +6644,8 @@ public void Checksum_04() // Test invalid UTF-8 encoded to base64 var builder = new BlobBuilder(); - // all zeros checksum and zero line and column numbers (which are also invalid, but won't be foudn with this particular test) - builder.WriteBytes(value: 0, byteCount: 12); + // all zeros checksum and zero position + builder.WriteBytes(value: 0, byteCount: 20); // write invalid utf-8 builder.WriteByte(0xc0); From db13fb5673c974b12f348ce2319dc50caba4c753 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 14:24:28 -0700 Subject: [PATCH 04/22] pass cancellation token --- src/Compilers/CSharp/Portable/CSharpExtensions.cs | 4 ++-- .../CSharp/Portable/Compilation/CSharpSemanticModel.cs | 4 ++-- src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpExtensions.cs b/src/Compilers/CSharp/Portable/CSharpExtensions.cs index 1c83da09a88fe..77e38484b55f7 100644 --- a/src/Compilers/CSharp/Portable/CSharpExtensions.cs +++ b/src/Compilers/CSharp/Portable/CSharpExtensions.cs @@ -1644,10 +1644,10 @@ public static Conversion ClassifyConversion(this SemanticModel? semanticModel, i /// 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) + public static InterceptableLocation? GetInterceptableLocation(this SemanticModel? semanticModel, InvocationExpressionSyntax node, CancellationToken cancellationToken = default) { var csModel = semanticModel as CSharpSemanticModel; - return csModel?.GetInterceptableLocation(node); + return csModel?.GetInterceptableLocation(node, cancellationToken); } /// diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs index af022bfeba3ed..fd2f1ac7e72aa 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs @@ -5216,7 +5216,7 @@ protected sealed override ISymbol GetDeclaredSymbolCore(SyntaxNode node, Cancell } #pragma warning disable RSEXPERIMENTAL002 // Internal usage of experimental API - public InterceptableLocation? GetInterceptableLocation(InvocationExpressionSyntax node) + public InterceptableLocation? GetInterceptableLocation(InvocationExpressionSyntax node, CancellationToken cancellationToken) { CheckSyntaxNode(node); if (node.GetInterceptableNameSyntax() is not { } nameSyntax) @@ -5225,7 +5225,7 @@ protected sealed override ISymbol GetDeclaredSymbolCore(SyntaxNode node, Cancell } var tree = node.SyntaxTree; - var text = tree.GetText(); + var text = tree.GetText(cancellationToken); var path = tree.FilePath; var checksum = text.GetContentHash(); diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index 70b883244e9d8..5dd238b29ebb9 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -22,5 +22,5 @@ static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.CreateTokenParser(Microsoft.C 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]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) -> Microsoft.CodeAnalysis.CSharp.InterceptableLocation? +[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! From 2cb8bfd35a718e4533e6c3b1fe8ad184fd597c26 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 14:27:31 -0700 Subject: [PATCH 05/22] Pass token --- .../Test/Semantic/SourceGeneration/GeneratorDriverTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs index 70c83d56005e5..5bc7afbc0cc23 100644 --- a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs @@ -4137,7 +4137,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: (GeneratorSyntaxContext context, CancellationToken token) => { var model = context.SemanticModel; - var locationSpecifier = model.GetInterceptableLocation((InvocationExpressionSyntax)context.Node); + 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. From 355728a749c589303499438c05e6df5ead85f9ec Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 14:31:25 -0700 Subject: [PATCH 06/22] Update baseline --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 247e1c3cdd27a..784276a0b88c9 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -6665,9 +6665,9 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. - // [InterceptsLocation(1, "AAAAAAAAAAAAAAAAwA==")] - Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, @"""AAAAAAAAAAAAAAAAwA==""").WithLocation(6, 28)); + // Interceptors.cs(6,28): error CS9233: Cannot intercept a call in file '�' because a matching file was not found in the compilation. + // [InterceptsLocation(1, "{base64}")] + Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, $@"""{base64}""").WithArguments("�").WithLocation(6, 28)); } [Theory] From 2394377223fcf930a8322c038f7952ac0fe5871c Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 14:33:51 -0700 Subject: [PATCH 07/22] update interceptors.md --- docs/features/interceptors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 1b9b816eb3ab5..7397b6545cfdb 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -87,7 +87,7 @@ The location of the call is the location of the simple name syntax which denotes #### Attribute creation -Roslyn provides a convenience API, `GetInterceptableLocation(this SemanticModel, InvocationExpressionSyntax)` for inserting `[InterceptsLocation]` into generated source code. We recommend that source generators depend on this API in order to intercept calls. +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 From 4b147f716e16c902c500924ce495624f2bfeff8a Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 15:18:45 -0700 Subject: [PATCH 08/22] Fix nullable warning --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 784276a0b88c9..865889b7206ed 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -3053,7 +3053,7 @@ public static void Main() """, options: RegularWithInterceptors); // test unexpected position within interceptable name token - var interceptableName = sourceTree.GetRoot().DescendantNodes().OfType().Last().GetInterceptableNameSyntax(); + var interceptableName = sourceTree.GetRoot().DescendantNodes().OfType().Last().GetInterceptableNameSyntax()!; var position = interceptableName.Position + 1; var builder = new BlobBuilder(); From 231c190bc5557e49e4daff46c46abbf725f16152 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 15:41:41 -0700 Subject: [PATCH 09/22] remove reference to line/column encoding --- docs/features/interceptors.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 7397b6545cfdb..93540e1940d3e 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -81,8 +81,6 @@ The "version 1" data encoding is a base64-encoded string consisting of the follo #### Position -Line and column numbers in the "version 1" data encoding 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 From 1056037f712d051806056dc8332299d71d06cf9b Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 1 Apr 2024 16:11:05 -0700 Subject: [PATCH 10/22] more feedback --- .../Portable/Compilation/CSharpCompilation.cs | 15 ----------- .../Portable/Utilities/ContentHashComparer.cs | 27 +++++++++++++++++++ .../Utilities/InterceptableLocation.cs | 13 ++++----- 3 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index f9038d840b67f..f5acf63631d31 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -1080,21 +1080,6 @@ ImmutableSegmentedDictionary> computeMappedPathToS } } - private class ContentHashComparer : IEqualityComparer> - { - public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) - { - return x.Span.SequenceEqual(y.Span); - } - - public int GetHashCode(ReadOnlyMemory obj) - { - return BinaryPrimitives.ReadInt32LittleEndian(obj.Span); - } - - public static ContentHashComparer Instance { get; } = new ContentHashComparer(); - } - internal OneOrMany GetSyntaxTreesByContentHash(ReadOnlyMemory contentHash) { var contentHashToSyntaxTree = _contentHashToSyntaxTree; diff --git a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs new file mode 100644 index 0000000000000..90fad1483c357 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs @@ -0,0 +1,27 @@ +// 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.IO; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp; + +internal class ContentHashComparer : IEqualityComparer> +{ + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + { + return x.Span.SequenceEqual(y.Span); + } + + public int GetHashCode(ReadOnlyMemory obj) + { + return BinaryPrimitives.ReadInt32LittleEndian(obj.Span); + } + + public static ContentHashComparer Instance { get; } = new ContentHashComparer(); +} diff --git a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs index b187d33ed967e..a78d3f4718fb6 100644 --- a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.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.Immutable; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -48,6 +49,11 @@ internal sealed class InterceptableLocation1 : InterceptableLocation internal InterceptableLocation1(ImmutableArray checksum, string path, int position, int lineNumberOneIndexed, int characterNumberOneIndexed) { + if (checksum.Length != 16) + { + throw new ArgumentException(message: "checksum must be exactly 16 bytes in length", paramName: nameof(checksum)); + } + _checksum = checksum; _path = path; _position = position; @@ -66,11 +72,6 @@ public override string Data { get { - if (_checksum.Length != 16) - { - throw new InvalidOperationException(); - } - var builder = new BlobBuilder(); builder.WriteBytes(_checksum, start: 0, 16); builder.WriteInt32(_position); @@ -100,7 +101,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { return Hash.Combine( - Hash.GetFNVHashCode(_checksum), + BinaryPrimitives.ReadInt32LittleEndian(_checksum.AsSpan()), Hash.Combine( _path.GetHashCode(), Hash.Combine( From ac01bc4fa51875c7099cf8e5daeb0df1819d6648 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 2 Apr 2024 10:27:44 -0700 Subject: [PATCH 11/22] Update src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs Co-authored-by: Jared Parsons --- src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs index 90fad1483c357..8e80c89755bea 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.CSharp; -internal class ContentHashComparer : IEqualityComparer> +internal sealed class ContentHashComparer : IEqualityComparer> { public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) { From 282bff3a563e95b1dbd7bbda7cffd6b0adfffff3 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 2 Apr 2024 11:31:09 -0700 Subject: [PATCH 12/22] Address feedback --- .../Utilities/InterceptableLocation.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs index a78d3f4718fb6..165f536b0a1bc 100644 --- a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Reflection.Metadata; +using Microsoft.Cci; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp; @@ -46,6 +47,7 @@ internal sealed class InterceptableLocation1 : InterceptableLocation private readonly int _position; private readonly int _lineNumberOneIndexed; private readonly int _characterNumberOneIndexed; + private string? _data; internal InterceptableLocation1(ImmutableArray checksum, string path, int position, int lineNumberOneIndexed, int characterNumberOneIndexed) { @@ -72,14 +74,24 @@ public override string Data { get { - var builder = new BlobBuilder(); - builder.WriteBytes(_checksum, start: 0, 16); - builder.WriteInt32(_position); + if (_data is null) + _data = makeData(); - var displayFileName = Path.GetFileName(_path); - builder.WriteUTF8(displayFileName); + return _data; - return Convert.ToBase64String(builder.ToArray()); + 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); + } } } From 5f85fcb72fed3e9910bae8a49225cfe921bbfdee Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 4 Apr 2024 10:26:58 -0700 Subject: [PATCH 13/22] Apply suggestions from code review Co-authored-by: Jan Jones --- docs/features/interceptors.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 93540e1940d3e..b0b8597f4f3f7 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -28,14 +28,14 @@ class C // generated code static class D { - [InterceptsLocation(version: 1, data: "...(refers to the call at L1")] + [InterceptsLocation(version: 1, data: "...(refers to the call at L1)")] public static void InterceptorMethod(this C c, int param) { Console.WriteLine($"interceptor {param}"); } - [InterceptsLocation(version: 1, data: "...(refers to the call at L2")] - [InterceptsLocation(version: 1, data: "...(refers to the call at L3")] + [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}"); From 8ac441d294370c75e9e3255e6b6b72f2c673f24c Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 4 Apr 2024 11:02:23 -0700 Subject: [PATCH 14/22] Apply diagnostics to attribute name in all cases to reduce churn --- .../SourceMethodSymbolWithAttributes.cs | 37 +++++++++---------- .../Semantic/Semantics/InterceptorsTests.cs | 32 ++++++++-------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index c4be2578985c8..78cbabe35f714 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -963,15 +963,18 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArguments arguments, int version, string? data) { var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; + var attributeNameSyntax = arguments.AttributeSyntaxOpt!.Name; // used for reporting diagnostics + var attributeLocation = attributeNameSyntax.Location; + if (version != 1) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationUnsupportedVersion, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 0)); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationUnsupportedVersion, attributeLocation); return; } if (data is null) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); return; } @@ -982,7 +985,7 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum } catch (FormatException) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); return; } @@ -999,7 +1002,7 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum if (bytes.Length < minLength) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); return; } @@ -1013,20 +1016,16 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum } catch (ArgumentException) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, arguments.Attribute.GetAttributeArgumentLocation(parameterIndex: 1)); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); return; } - var attributeSyntax = arguments.AttributeSyntaxOpt; - Debug.Assert(attributeSyntax is object); - var attributeLocation = attributeSyntax.Location; - - var interceptorsNamespaces = ((CSharpParseOptions)attributeSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; - var thisNamespaceNames = getNamespaceNames(); + var interceptorsNamespaces = ((CSharpParseOptions)attributeNameSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; + var thisNamespaceNames = getNamespaceNames(this); var foundAnyMatch = interceptorsNamespaces.Any(ns => isDeclaredInNamespace(thisNamespaceNames, ns)); if (!foundAnyMatch) { - reportFeatureNotEnabled(diagnostics, attributeSyntax, thisNamespaceNames); + reportFeatureNotEnabled(diagnostics, attributeLocation, thisNamespaceNames); thisNamespaceNames.Free(); return; } @@ -1057,13 +1056,13 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum var matchingTrees = DeclaringCompilation.GetSyntaxTreesByContentHash(hash); if (matchingTrees.Count > 1) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDuplicateFile, attributeData.GetAttributeArgumentLocation(1), displayFileName); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDuplicateFile, attributeLocation, displayFileName); return; } if (matchingTrees.Count == 0) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationFileNotFound, attributeData.GetAttributeArgumentLocation(1), displayFileName); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationFileNotFound, attributeLocation, displayFileName); return; } @@ -1106,10 +1105,10 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum DeclaringCompilation.AddInterception(matchingTree.FilePath, position, attributeLocation, this); // Caller must free the returned builder. - ArrayBuilder getNamespaceNames() + ArrayBuilder getNamespaceNames(SourceMethodSymbolWithAttributes @this) { var namespaceNames = ArrayBuilder.GetInstance(); - for (var containingNamespace = ContainingNamespace; containingNamespace?.IsGlobalNamespace == false; containingNamespace = containingNamespace.ContainingNamespace) + 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"] @@ -1141,16 +1140,16 @@ static bool isDeclaredInNamespace(ArrayBuilder thisNamespaceNames, Immut return true; } - static void reportFeatureNotEnabled(BindingDiagnosticBag diagnostics, AttributeSyntax attributeSyntax, ArrayBuilder namespaceNames) + static void reportFeatureNotEnabled(BindingDiagnosticBag diagnostics, Location attributeLocation, ArrayBuilder namespaceNames) { if (namespaceNames.Count == 0) { - diagnostics.Add(ErrorCode.ERR_InterceptorGlobalNamespace, attributeSyntax); + diagnostics.Add(ErrorCode.ERR_InterceptorGlobalNamespace, attributeLocation); } else { var recommendedProperty = $"$(InterceptorsPreviewNamespaces);{string.Join(".", namespaceNames)}"; - diagnostics.Add(ErrorCode.ERR_InterceptorsFeatureNotEnabled, attributeSyntax, recommendedProperty); + diagnostics.Add(ErrorCode.ERR_InterceptorsFeatureNotEnabled, attributeLocation, recommendedProperty); } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 865889b7206ed..27db22cab39da 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -6633,9 +6633,9 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. + // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. // [InterceptsLocation(1, "jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===")] - Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, @"""jB4qgCy292LkEGCwmD+R6AcAAAAJAAAAUHJvZ3JhbS5jcw===""").WithLocation(6, 28)); + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); } [Fact] @@ -6665,9 +6665,9 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,28): error CS9233: Cannot intercept a call in file '�' because a matching file was not found in the compilation. - // [InterceptsLocation(1, "{base64}")] - Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, $@"""{base64}""").WithArguments("�").WithLocation(6, 28)); + // Interceptors.cs(6,6): error CS9233: Cannot intercept a call in file '�' because a matching file was not found in the compilation. + // [InterceptsLocation(1, "AAAAAAAAAAAAAAAAAAAAAAAAAADA")] + Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, "InterceptsLocation").WithArguments("�").WithLocation(6, 6)); } [Theory] @@ -6689,9 +6689,9 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. - // [InterceptsLocation(1, "{data}")] - Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, $@"""{data}""").WithLocation(6, 28)); + // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. + // [InterceptsLocation(1, "")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); } [Fact] @@ -6711,9 +6711,9 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,28): error CS9229: The data argument to InterceptsLocationAttribute is not in the correct format. + // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. // [InterceptsLocation(1, null)] - Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "null").WithLocation(6, 28)); + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); } [Fact] @@ -6751,9 +6751,9 @@ static class Interceptors var comp1 = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp1.VerifyEmitDiagnostics( - // Interceptors.cs(6,28): error CS9232: Cannot intercept a call in file 'Program.cs' because a matching file was not found in the compilation. - // [InterceptsLocation(1, "{locationSpecifier.Data}")] - Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, $@"""{locationSpecifier.Data}""").WithArguments("Program.cs").WithLocation(6, 28)); + // Interceptors.cs(6,6): error CS9233: 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] @@ -6796,8 +6796,8 @@ static class Interceptors interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp1.GetDiagnostics().Where(d => d.Location.SourceTree == interceptors).Verify( - // Interceptors.cs(6,28): error CS9231: Cannot intercept a call in file 'Program1.cs' because it is duplicated elsewhere in the compilation. - // [InterceptsLocation(1, "{data}")] - Diagnostic(ErrorCode.ERR_InterceptsLocationDuplicateFile, $@"""{locationSpecifier.Data}""").WithArguments("Program1.cs").WithLocation(6, 28)); + // Interceptors.cs(6,6): error CS9232: 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)); } } From 9763c4fb4ace43fb41689112642cf17e17a18258 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 4 Apr 2024 11:02:48 -0700 Subject: [PATCH 15/22] Add tests to exercise 'interceptable syntactically' concept --- .../Semantic/Semantics/InterceptorsTests.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 27db22cab39da..e1a19bc80ae7b 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -6800,4 +6800,76 @@ static class Interceptors // [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); + } } From 5bef2bc76a001b20afc8ab64ff84ca6a41bffdff Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 4 Apr 2024 15:55:57 -0700 Subject: [PATCH 16/22] address some feedback. Port tests for newly added code paths. --- .../Portable/Compilation/CSharpCompilation.cs | 2 + .../Compilation/CSharpSemanticModel.cs | 8 +- .../SourceMethodSymbolWithAttributes.cs | 21 +- .../Portable/Utilities/ContentHashComparer.cs | 3 +- .../Utilities/InterceptableLocation.cs | 24 +- .../Semantic/Semantics/InterceptorsTests.cs | 323 +++++++++++++++++- 6 files changed, 354 insertions(+), 27 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index f5acf63631d31..04b74a1dbd441 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -1082,6 +1082,8 @@ ImmutableSegmentedDictionary> computeMappedPathToS internal OneOrMany GetSyntaxTreesByContentHash(ReadOnlyMemory contentHash) { + Debug.Assert(contentHash.Length == InterceptableLocation1.ContentHashLength); + var contentHashToSyntaxTree = _contentHashToSyntaxTree; if (contentHashToSyntaxTree.IsDefault) { diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs index fd2f1ac7e72aa..4e31872c295c1 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs @@ -5224,7 +5224,13 @@ protected sealed override ISymbol GetDeclaredSymbolCore(SyntaxNode node, Cancell return null; } - var tree = node.SyntaxTree; + 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(); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 78cbabe35f714..adc08c149574e 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -968,7 +968,7 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum if (version != 1) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationUnsupportedVersion, attributeLocation); + diagnostics.Add(ErrorCode.ERR_InterceptsLocationUnsupportedVersion, attributeLocation, version); return; } @@ -1009,16 +1009,14 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum var hash = bytes.AsMemory(start: hashIndex, length: hashSize); var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: positionIndex)); - string displayFileName; - try - { - displayFileName = Encoding.UTF8.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); - } - catch (ArgumentException) - { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); - return; - } + // https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.utf8?view=net-8.0#remarks states: + // > `Encoding.UTF8` returns a UTF8Encoding object that uses replacement fallback to replace each string that it can't encode and each byte that it can't decode with a question mark ("?") character. + // + // If these assertions are violated, it means the encoder may throw for invalid UTF-8 sequences, which we don't expect. + // In this case we would either need to adjust the code to start handling ArgumentException from GetString, or create a static UTF8 decoder with non-throwing behavior. + Debug.Assert(Encoding.UTF8.DecoderFallback is DecoderReplacementFallback); + Debug.Assert(Encoding.UTF8.IsReadOnly); + string displayFileName = Encoding.UTF8.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); var interceptorsNamespaces = ((CSharpParseOptions)attributeNameSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; var thisNamespaceNames = getNamespaceNames(this); @@ -1089,6 +1087,7 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum 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: diff --git a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs index 8e80c89755bea..3640652f7e60d 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs @@ -5,7 +5,6 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; -using System.IO; using System.Linq; using Roslyn.Utilities; @@ -20,6 +19,8 @@ public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) 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 index 165f536b0a1bc..8d6dfe7bd0b0b 100644 --- a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -5,16 +5,17 @@ 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.Reflection.Metadata; using Microsoft.Cci; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp; [Experimental(RoslynExperiments.Interceptors, UrlFormat = RoslynExperiments.Interceptors_Url)] +//[DebuggerDisplay("{GetDebuggerDisplay(), nq}")] public abstract class InterceptableLocation { private protected InterceptableLocation() { } @@ -42,19 +43,22 @@ private protected InterceptableLocation() { } /// internal sealed class InterceptableLocation1 : InterceptableLocation { + internal const int ContentHashLength = 16; + private readonly ImmutableArray _checksum; private readonly string _path; private readonly int _position; private readonly int _lineNumberOneIndexed; private readonly int _characterNumberOneIndexed; - private string? _data; + private string? _lazyData; internal InterceptableLocation1(ImmutableArray checksum, string path, int position, int lineNumberOneIndexed, int characterNumberOneIndexed) { - if (checksum.Length != 16) - { - throw new ArgumentException(message: "checksum must be exactly 16 bytes in length", paramName: nameof(checksum)); - } + 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; @@ -69,15 +73,17 @@ public override string GetDisplayLocation() return $"{_path}({_lineNumberOneIndexed},{_characterNumberOneIndexed})"; } + public override string ToString() => GetDisplayLocation(); + public override int Version => 1; public override string Data { get { - if (_data is null) - _data = makeData(); + if (_lazyData is null) + _lazyData = makeData(); - return _data; + return _lazyData; string makeData() { diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index e1a19bc80ae7b..11a6b3cd753f6 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -8,13 +8,13 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Linq.Expressions; using System.Reflection.Metadata; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; @@ -130,6 +130,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() { @@ -1793,6 +1856,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() { @@ -1832,6 +1951,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() { @@ -1949,6 +2126,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() { @@ -3076,8 +3298,8 @@ static class D var comp = CreateCompilation([sourceTree, interceptorTree, s_attributesTree]); comp.VerifyEmitDiagnostics( // (6,6): error CS9234: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. - // [InterceptsLocation(1, "{base64}")] - Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, $@"InterceptsLocation(1, ""{base64}"")").WithArguments("Error").WithLocation(6, 6) + // [InterceptsLocation(1, "ExWKMussA+NMlN5J0QNXiEMBAABFcnJvcg==")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, "InterceptsLocation").WithArguments("Error").WithLocation(6, 6) ); } @@ -3127,8 +3349,8 @@ static class D var comp = CreateCompilation([sourceTree, interceptorTree, s_attributesTree]); comp.VerifyEmitDiagnostics( // (6,6): error CS9234: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. - // [InterceptsLocation(1, "{base64}")] - Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, $@"InterceptsLocation(1, ""{base64}"")").WithArguments("Error").WithLocation(6, 6) + // [InterceptsLocation(1, "ExWKMussA+NMlN5J0QNXiJ+GAQBFcnJvcg==")] + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, "InterceptsLocation").WithArguments("Error").WithLocation(6, 6) ); } @@ -5434,6 +5656,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() { @@ -6872,4 +7133,56 @@ static void Main() 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 CS9231: 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()); + } } From 499fd186b660f4bb340a2cd46674bac2388a7ac7 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 4 Apr 2024 16:10:22 -0700 Subject: [PATCH 17/22] Decode next to encode --- .../SourceMethodSymbolWithAttributes.cs | 43 +-------------- .../Utilities/InterceptableLocation.cs | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index adc08c149574e..ed98594b325f4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -972,52 +972,11 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum return; } - if (data is null) + if (InterceptableLocation1.Decode(data, attributeLocation, diagnostics) is not var (hash, position, displayFileName)) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); return; } - byte[] bytes; - try - { - bytes = Convert.FromBase64String(data); - } - catch (FormatException) - { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); - return; - } - - // 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) - { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, attributeLocation); - return; - } - - var hash = bytes.AsMemory(start: hashIndex, length: hashSize); - var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: positionIndex)); - - // https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.utf8?view=net-8.0#remarks states: - // > `Encoding.UTF8` returns a UTF8Encoding object that uses replacement fallback to replace each string that it can't encode and each byte that it can't decode with a question mark ("?") character. - // - // If these assertions are violated, it means the encoder may throw for invalid UTF-8 sequences, which we don't expect. - // In this case we would either need to adjust the code to start handling ArgumentException from GetString, or create a static UTF8 decoder with non-throwing behavior. - Debug.Assert(Encoding.UTF8.DecoderFallback is DecoderReplacementFallback); - Debug.Assert(Encoding.UTF8.IsReadOnly); - string displayFileName = Encoding.UTF8.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); - var interceptorsNamespaces = ((CSharpParseOptions)attributeNameSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; var thisNamespaceNames = getNamespaceNames(this); var foundAnyMatch = interceptorsNamespaces.Any(ns => isDeclaredInNamespace(thisNamespaceNames, ns)); diff --git a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs index 8d6dfe7bd0b0b..c4d4ecc7196a0 100644 --- a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Text; using Microsoft.Cci; using Roslyn.Utilities; @@ -101,6 +102,57 @@ string makeData() } } + internal static (ImmutableArray checksum, int position, string displayFileName)? Decode(string? data, Location diagnosticLocation, BindingDiagnosticBag diagnostics) + { + if (data is null) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, diagnosticLocation); + return null; + } + + byte[] bytes; + try + { + bytes = Convert.FromBase64String(data); + } + catch (FormatException) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, diagnosticLocation); + 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) + { + diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, diagnosticLocation); + return null; + } + + var hash = bytes.AsMemory(start: hashIndex, length: hashSize); + var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: positionIndex)); + + // https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.utf8?view=net-8.0#remarks states: + // > `Encoding.UTF8` returns a UTF8Encoding object that uses replacement fallback to replace each string that it can't encode and each byte that it can't decode with a question mark ("?") character. + // + // If these assertions are violated, it means the encoder may throw for invalid UTF-8 sequences, which we don't expect. + // In this case we would either need to adjust the code to start handling ArgumentException from GetString, or create a static UTF8 decoder with non-throwing behavior. + Debug.Assert(Encoding.UTF8.DecoderFallback is DecoderReplacementFallback); + Debug.Assert(Encoding.UTF8.IsReadOnly); + string displayFileName = Encoding.UTF8.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); + + 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) From e1f927386e5bf0acdc654adc4eda9cdf05e798f5 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 4 Apr 2024 16:22:58 -0700 Subject: [PATCH 18/22] fix signature --- .../CSharp/Portable/Utilities/InterceptableLocation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs index c4d4ecc7196a0..bbdd7171d6358 100644 --- a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -102,7 +102,7 @@ string makeData() } } - internal static (ImmutableArray checksum, int position, string displayFileName)? Decode(string? data, Location diagnosticLocation, BindingDiagnosticBag diagnostics) + internal static (ReadOnlyMemory checksum, int position, string displayFileName)? Decode(string? data, Location diagnosticLocation, BindingDiagnosticBag diagnostics) { if (data is null) { From 13ece53cd1145f437d32ac5738ccbe8e6f85b029 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Sun, 7 Apr 2024 12:06:30 -0700 Subject: [PATCH 19/22] Address feedback --- .../CSharp/Portable/CSharpExtensions.cs | 2 +- .../CSharp/Portable/PublicAPI.Unshipped.txt | 2 + .../SourceMethodSymbolWithAttributes.cs | 20 +++++----- .../Portable/Utilities/ContentHashComparer.cs | 6 ++- .../Utilities/InterceptableLocation.cs | 39 +++++++++---------- .../Semantic/Semantics/InterceptorsTests.cs | 38 +++++++++++------- 6 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/Compilers/CSharp/Portable/CSharpExtensions.cs b/src/Compilers/CSharp/Portable/CSharpExtensions.cs index 77e38484b55f7..745c036c18d99 100644 --- a/src/Compilers/CSharp/Portable/CSharpExtensions.cs +++ b/src/Compilers/CSharp/Portable/CSharpExtensions.cs @@ -1640,7 +1640,7 @@ public static Conversion ClassifyConversion(this SemanticModel? semanticModel, i } /// - /// If 'node' cannot be intercepted syntactically, returns null. + /// 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)] diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index 5dd238b29ebb9..e2eac632d971c 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -21,6 +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 ed98594b325f4..a4c5ed6fff70e 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -963,7 +963,8 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArguments arguments, int version, string? data) { var diagnostics = (BindingDiagnosticBag)arguments.Diagnostics; - var attributeNameSyntax = arguments.AttributeSyntaxOpt!.Name; // used for reporting diagnostics + Debug.Assert(arguments.AttributeSyntaxOpt is not null); + var attributeNameSyntax = arguments.AttributeSyntaxOpt.Name; // used for reporting diagnostics var attributeLocation = attributeNameSyntax.Location; if (version != 1) @@ -972,14 +973,15 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum return; } - if (InterceptableLocation1.Decode(data, attributeLocation, diagnostics) is not var (hash, position, displayFileName)) + 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(ns => isDeclaredInNamespace(thisNamespaceNames, ns)); + var foundAnyMatch = interceptorsNamespaces.Any(static (ns, thisNamespaceNames) => isDeclaredInNamespace(thisNamespaceNames, ns), thisNamespaceNames); if (!foundAnyMatch) { reportFeatureNotEnabled(diagnostics, attributeLocation, thisNamespaceNames); @@ -988,8 +990,6 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum } thisNamespaceNames.Free(); - var attributeData = arguments.Attribute; - if (ContainingType.IsGenericType) { diagnostics.Add(ErrorCode.ERR_InterceptorContainingTypeCannotBeGeneric, attributeLocation, this); @@ -1027,7 +1027,7 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum SyntaxTree? matchingTree = matchingTrees[0]; var root = matchingTree.GetRoot(); - if (position > root.EndPosition) + if (position < 0 || position > root.EndPosition) { diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidPosition, attributeLocation, displayFileName); return; @@ -1063,7 +1063,7 @@ private void DecodeInterceptsLocationChecksumBased(DecodeWellKnownAttributeArgum DeclaringCompilation.AddInterception(matchingTree.FilePath, position, attributeLocation, this); // Caller must free the returned builder. - ArrayBuilder getNamespaceNames(SourceMethodSymbolWithAttributes @this) + static ArrayBuilder getNamespaceNames(SourceMethodSymbolWithAttributes @this) { var namespaceNames = ArrayBuilder.GetInstance(); for (var containingNamespace = @this.ContainingNamespace; containingNamespace?.IsGlobalNamespace == false; containingNamespace = containingNamespace.ContainingNamespace) @@ -1123,9 +1123,9 @@ private void DecodeInterceptsLocationAttributeExperimentalCompat( var attributeSyntax = arguments.AttributeSyntaxOpt; Debug.Assert(attributeSyntax is object); var attributeLocation = attributeSyntax.Location; - int filePathParameterIndex = 0; - int lineNumberParameterIndex = 1; - int characterNumberParameterIndex = 2; + const int filePathParameterIndex = 0; + const int lineNumberParameterIndex = 1; + const int characterNumberParameterIndex = 2; var interceptorsNamespaces = ((CSharpParseOptions)attributeSyntax.SyntaxTree.Options).InterceptorsPreviewNamespaces; var thisNamespaceNames = getNamespaceNames(); diff --git a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs index 3640652f7e60d..4bc6b50362f96 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ContentHashComparer.cs @@ -12,6 +12,10 @@ 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); @@ -23,6 +27,4 @@ public int GetHashCode(ReadOnlyMemory obj) // Therefore simply reading the first 4 bytes of it results in an adequate hash code. return BinaryPrimitives.ReadInt32LittleEndian(obj.Span); } - - public static ContentHashComparer Instance { get; } = new ContentHashComparer(); } diff --git a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs index bbdd7171d6358..4f3a23634d307 100644 --- a/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs +++ b/src/Compilers/CSharp/Portable/Utilities/InterceptableLocation.cs @@ -16,7 +16,6 @@ namespace Microsoft.CodeAnalysis.CSharp; [Experimental(RoslynExperiments.Interceptors, UrlFormat = RoslynExperiments.Interceptors_Url)] -//[DebuggerDisplay("{GetDebuggerDisplay(), nq}")] public abstract class InterceptableLocation { private protected InterceptableLocation() { } @@ -36,6 +35,9 @@ private protected InterceptableLocation() { } /// 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 @@ -45,6 +47,7 @@ private protected InterceptableLocation() { } 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; @@ -102,11 +105,10 @@ string makeData() } } - internal static (ReadOnlyMemory checksum, int position, string displayFileName)? Decode(string? data, Location diagnosticLocation, BindingDiagnosticBag diagnostics) + internal static (ReadOnlyMemory checksum, int position, string displayFileName)? Decode(string? data) { if (data is null) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, diagnosticLocation); return null; } @@ -117,7 +119,6 @@ internal static (ReadOnlyMemory checksum, int position, string displayFile } catch (FormatException) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, diagnosticLocation); return null; } @@ -134,21 +135,21 @@ internal static (ReadOnlyMemory checksum, int position, string displayFile if (bytes.Length < minLength) { - diagnostics.Add(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, diagnosticLocation); return null; } var hash = bytes.AsMemory(start: hashIndex, length: hashSize); var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(start: positionIndex)); - // https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.utf8?view=net-8.0#remarks states: - // > `Encoding.UTF8` returns a UTF8Encoding object that uses replacement fallback to replace each string that it can't encode and each byte that it can't decode with a question mark ("?") character. - // - // If these assertions are violated, it means the encoder may throw for invalid UTF-8 sequences, which we don't expect. - // In this case we would either need to adjust the code to start handling ArgumentException from GetString, or create a static UTF8 decoder with non-throwing behavior. - Debug.Assert(Encoding.UTF8.DecoderFallback is DecoderReplacementFallback); - Debug.Assert(Encoding.UTF8.IsReadOnly); - string displayFileName = Encoding.UTF8.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); + string displayFileName; + try + { + displayFileName = s_encoding.GetString(bytes, index: displayNameIndex, count: bytes.Length - displayNameIndex); + } + catch (ArgumentException) + { + return null; + } return (hash, position, displayFileName); } @@ -170,14 +171,10 @@ public override bool Equals(object? obj) 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()), - Hash.Combine( - _path.GetHashCode(), - Hash.Combine( - _position, - Hash.Combine( - _lineNumberOneIndexed, - _characterNumberOneIndexed)))); + BinaryPrimitives.ReadInt32LittleEndian(_checksum.AsSpan()), + _position); } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 11a6b3cd753f6..59bdf606f5156 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -14,7 +14,6 @@ using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Test.Utilities; -using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; @@ -3303,8 +3302,10 @@ static class D ); } - [Fact] - public void InterceptsLocationBadPosition_Checksum_02() + [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; @@ -3326,9 +3327,6 @@ public static void Main() } """, options: RegularWithInterceptors); - // test position past end of the file - var position = 99999; - var builder = new BlobBuilder(); builder.WriteBytes(sourceTree.GetText().GetContentHash()); builder.WriteInt32(position); @@ -6853,17 +6851,23 @@ class C static void Main() { M(); + M(); } } """.NormalizeLineEndings(), "path/to/Program.cs", RegularWithInterceptors); var comp = CreateCompilation(tree); var model = comp.GetSemanticModel(tree); - var node = tree.GetRoot().DescendantNodes().OfType().Single(); - var locationSpecifier = model.GetInterceptableLocation(node)!; + 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("path/to/Program.cs(7,9)", locationSpecifier!.GetDisplayLocation()); Assert.Equal(1, locationSpecifier.Version); Assert.Equal(locationSpecifier, locationSpecifier); @@ -6873,8 +6877,16 @@ static void Main() // 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("jB4qgCy292LkEGCwmD+R6FIAAABQcm9ncmFtLmNz", locationSpecifier.Data); - AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "jB4qgCy292LkEGCwmD+R6FIAAABQcm9ncmFtLmNz")]""", locationSpecifier.GetInterceptsLocationAttributeSyntax()); + 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()); + AssertEx.Equal("xRCCFCvTOZMORzSr/fZQFmAAAABQcm9ncmFtLmNz", otherLocation.Data); + AssertEx.Equal("""[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "xRCCFCvTOZMORzSr/fZQFmAAAABQcm9ncmFtLmNz")]""", otherLocation.GetInterceptsLocationAttributeSyntax()); + } [Fact] @@ -6926,9 +6938,9 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,6): error CS9233: Cannot intercept a call in file '�' because a matching file was not found in the compilation. + // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. // [InterceptsLocation(1, "AAAAAAAAAAAAAAAAAAAAAAAAAADA")] - Diagnostic(ErrorCode.ERR_InterceptsLocationFileNotFound, "InterceptsLocation").WithArguments("�").WithLocation(6, 6)); + Diagnostic(ErrorCode.ERR_InterceptsLocationDataInvalidFormat, "InterceptsLocation").WithLocation(6, 6)); } [Theory] From 00e90be9db5c869fa01a0fcc38d0568c5a9566c6 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Sun, 7 Apr 2024 12:10:34 -0700 Subject: [PATCH 20/22] Adjust error codes --- src/Compilers/CSharp/Portable/Errors/ErrorCode.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index e82ce53e6e0f1..0642d20ad0220 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2303,11 +2303,12 @@ internal enum ErrorCode ERR_NoModifiersOnUsing = 9229, ERR_CannotDynamicInvokeOnExpression = 9230, - ERR_InterceptsLocationDataInvalidFormat = 9230, - ERR_InterceptsLocationUnsupportedVersion = 9231, - ERR_InterceptsLocationDuplicateFile = 9232, - ERR_InterceptsLocationFileNotFound = 9233, - ERR_InterceptsLocationDataInvalidPosition = 9234, + // TODO2: pack + ERR_InterceptsLocationDataInvalidFormat = 9250, + ERR_InterceptsLocationUnsupportedVersion = 9251, + ERR_InterceptsLocationDuplicateFile = 9252, + ERR_InterceptsLocationFileNotFound = 9253, + ERR_InterceptsLocationDataInvalidPosition = 9254, #endregion From 2d65d386c3ae24e4d8682f8d14c3549cdf940d52 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Sun, 7 Apr 2024 12:22:18 -0700 Subject: [PATCH 21/22] Also test other display location --- .../CSharp/Test/Semantic/Semantics/InterceptorsTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 59bdf606f5156..5b994b88a55f3 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -6884,6 +6884,8 @@ static void Main() 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()); From 923053639edb4a35a9c11b28483d139e1170c0dd Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Fri, 12 Apr 2024 12:47:21 -0700 Subject: [PATCH 22/22] pack --- .../CSharp/Portable/Errors/ErrorCode.cs | 11 +++++------ .../Semantic/Semantics/InterceptorsTests.cs | 18 +++++++++--------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 0642d20ad0220..15fee745c96a3 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2303,12 +2303,11 @@ internal enum ErrorCode ERR_NoModifiersOnUsing = 9229, ERR_CannotDynamicInvokeOnExpression = 9230, - // TODO2: pack - ERR_InterceptsLocationDataInvalidFormat = 9250, - ERR_InterceptsLocationUnsupportedVersion = 9251, - ERR_InterceptsLocationDuplicateFile = 9252, - ERR_InterceptsLocationFileNotFound = 9253, - ERR_InterceptsLocationDataInvalidPosition = 9254, + ERR_InterceptsLocationDataInvalidFormat = 9231, + ERR_InterceptsLocationUnsupportedVersion = 9232, + ERR_InterceptsLocationDuplicateFile = 9233, + ERR_InterceptsLocationFileNotFound = 9234, + ERR_InterceptsLocationDataInvalidPosition = 9235, #endregion diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 5b994b88a55f3..63b63f19b1d28 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -3296,7 +3296,7 @@ static class D """, options: RegularWithInterceptors); var comp = CreateCompilation([sourceTree, interceptorTree, s_attributesTree]); comp.VerifyEmitDiagnostics( - // (6,6): error CS9234: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. + // (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) ); @@ -3346,7 +3346,7 @@ static class D """, options: RegularWithInterceptors); var comp = CreateCompilation([sourceTree, interceptorTree, s_attributesTree]); comp.VerifyEmitDiagnostics( - // (6,6): error CS9234: The data argument to InterceptsLocationAttribute refers to an invalid position in file 'Error'. + // (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) ); @@ -6908,7 +6908,7 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. + // 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)); } @@ -6940,7 +6940,7 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. + // 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)); } @@ -6964,7 +6964,7 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. + // 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)); } @@ -6986,7 +6986,7 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,6): error CS9230: The data argument to InterceptsLocationAttribute is not in the correct format. + // 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)); } @@ -7026,7 +7026,7 @@ static class Interceptors var comp1 = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp1.VerifyEmitDiagnostics( - // Interceptors.cs(6,6): error CS9233: Cannot intercept a call in file 'Program.cs' because a matching file was not found in the compilation. + // 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)); } @@ -7071,7 +7071,7 @@ static class Interceptors interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp1.GetDiagnostics().Where(d => d.Location.SourceTree == interceptors).Verify( - // Interceptors.cs(6,6): error CS9232: Cannot intercept a call in file 'Program1.cs' because it is duplicated elsewhere in the compilation. + // 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)); } @@ -7169,7 +7169,7 @@ static class Interceptors var comp = CreateCompilation([interceptors, CSharpTestSource.Parse(s_attributesSource.text, s_attributesSource.path, RegularWithInterceptors)]); comp.VerifyEmitDiagnostics( - // Interceptors.cs(6,6): error CS9231: Version '0' of the interceptors format is not supported. The latest supported version is '1'. + // 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)); }