diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index 515b95e36d712..83ec2dd26a944 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -69,11 +69,20 @@ https://github.com/dotnet/roslyn/issues/67079 is a bug which causes file-local s #### File paths -File paths used in `[InterceptsLocation]` must exactly match the paths on the syntax trees they refer to by ordinal comparison. `SyntaxTree.FilePath` has already applied `/pathmap` substitution, so the paths used in the attribute will be less environment-specific in many projects. +File paths used in `[InterceptsLocation]` are expected to have `/pathmap` substitution already applied. Generators should accomplish this by locally recreating the file path transformation performed by the compiler: -The compiler does not map `#line` directives when determining if an `[InterceptsLocation]` attribute intercepts a particular call in syntax. +```cs +using Microsoft.CodeAnalysis; + +string GetInterceptorFilePath(SyntaxTree tree, Compilation compilation) +{ + return compilation.Options.SourceReferenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath; +} +``` -PROTOTYPE(ic): editorconfig support matches paths in cross-platform fashion (e.g. normalizing slashes). We should revisit how that works and consider if the same matching strategy should be used instead of ordinal comparison. +The file path given in the attribute must be equal by ordinal comparison to the value given by the above function. + +The compiler does not map `#line` directives when determining if an `[InterceptsLocation]` attribute intercepts a particular call in syntax. #### Position diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index ccb6d95a036d4..d9470ca18f2e0 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7517,6 +7517,9 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + The given file has '{0}' lines, which is fewer than the provided line number '{1}'. @@ -7541,9 +7544,6 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Signatures of interceptable and interceptor methods do not match. - - An interceptable method must be an ordinary member method. - An interceptor method must be an ordinary member method. diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index b2d5fd50f03ee..0930b80e07b8c 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -154,6 +154,9 @@ internal Conversions Conversions /// private readonly ConcurrentCache _typeToNullableVersion = new ConcurrentCache(size: 100); + /// Lazily caches SyntaxTrees by their mapped path. Used to look up the syntax tree referenced by an interceptor. + private ImmutableSegmentedDictionary> _mappedPathToSyntaxTree; + public override string Language { get @@ -1037,6 +1040,33 @@ internal override int GetSyntaxTreeOrdinal(SyntaxTree tree) } } + internal OneOrMany GetSyntaxTreesByMappedPath(string mappedPath) + { + // We could consider storing this on SyntaxAndDeclarationManager instead, and updating it incrementally. + // However, this would make it more difficult for it to be "pay-for-play", + // i.e. only created in compilations where interceptors are used. + var mappedPathToSyntaxTree = _mappedPathToSyntaxTree; + if (mappedPathToSyntaxTree.IsDefault) + { + RoslynImmutableInterlocked.InterlockedInitialize(ref _mappedPathToSyntaxTree, computeMappedPathToSyntaxTree()); + mappedPathToSyntaxTree = _mappedPathToSyntaxTree; + } + + return mappedPathToSyntaxTree.TryGetValue(mappedPath, out var value) ? value : OneOrMany.Empty; + + ImmutableSegmentedDictionary> computeMappedPathToSyntaxTree() + { + var builder = ImmutableSegmentedDictionary.CreateBuilder>(); + var resolver = Options.SourceReferenceResolver; + foreach (var tree in SyntaxTrees) + { + var path = resolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath; + builder[path] = builder.ContainsKey(path) ? builder[path].Add(tree) : OneOrMany.Create(tree); + } + return builder.ToImmutable(); + } + } + #endregion #region References diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 24da3889ccb85..07d22acb3f380 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2201,7 +2201,7 @@ internal enum ErrorCode ERR_InterceptorLineOutOfRange = 27005, ERR_InterceptorCharacterOutOfRange = 27006, ERR_InterceptorSignatureMismatch = 27007, - ERR_InterceptableMethodMustBeOrdinary = 27008, + ERR_InterceptorPathNotInCompilationWithUnmappedCandidate = 27008, ERR_InterceptorMethodMustBeOrdinary = 27009, ERR_InterceptorMustReferToStartOfTokenPosition = 27010, ERR_InterceptorMustHaveMatchingThisParameter = 27011, diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 68ead9b8db547..4929639467e01 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -2328,10 +2328,10 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_InterceptorCannotBeGeneric: case ErrorCode.ERR_InterceptorPathNotInCompilation: case ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate: + case ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate: case ErrorCode.ERR_InterceptorPositionBadToken: case ErrorCode.ERR_InterceptorLineOutOfRange: case ErrorCode.ERR_InterceptorCharacterOutOfRange: - case ErrorCode.ERR_InterceptableMethodMustBeOrdinary: case ErrorCode.ERR_InterceptorMethodMustBeOrdinary: case ErrorCode.ERR_InterceptorMustReferToStartOfTokenPosition: case ErrorCode.ERR_InterceptorFilePathCannotBeNull: diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 9d164bd43ab0f..b0b42303f4cc0 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -218,7 +218,7 @@ private void InterceptCallAndAdjustArguments( { // Special case when intercepting an extension method call in reduced form with a non-extension. this._diagnostics.Add(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, attributeLocation, method.Parameters[0], method); - // PROTOYPE(ic): use a symbol display format which includes the 'this' modifier? + // PROTOTYPE(ic): use a symbol display format which includes the 'this' modifier? //this._diagnostics.Add(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, attributeLocation, new FormattedSymbol(method.Parameters[0], SymbolDisplayFormat.CSharpErrorMessageFormat), method); return; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 25a9f2cb59fe9..18e3285aeadb2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -967,8 +967,8 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } - var filePath = (string?)attributeArguments[0].Value; - if (filePath is null) + var attributeFilePath = (string?)attributeArguments[0].Value; + if (attributeFilePath is null) { diagnostics.Add(ErrorCode.ERR_InterceptorFilePathCannotBeNull, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax)); return; @@ -987,42 +987,54 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments } var syntaxTrees = DeclaringCompilation.SyntaxTrees; - SyntaxTree? matchingTree = null; - // PROTOTYPE(ic): we need to resolve the paths before comparing (i.e. respect /pathmap). - // At that time, we should look at caching the resolved paths for the trees in a set (or maybe a Map>). - // so we can reduce the cost of these checks. - foreach (var tree in syntaxTrees) + var matchingTrees = DeclaringCompilation.GetSyntaxTreesByMappedPath(attributeFilePath); + if (matchingTrees.Count == 0) { - if (tree.FilePath == filePath) + var referenceResolver = DeclaringCompilation.Options.SourceReferenceResolver; + // if we expect '/_/Program.cs': + + // we might get: 'C:\Project\Program.cs' <-- path not mapped + var unmappedMatch = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath == filePath, attributeFilePath); + if (unmappedMatch != null) { - if (matchingTree == null) - { - matchingTree = tree; - // need to keep searching in case we find another tree with the same path - } - else - { - diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), filePath); - return; - } + diagnostics.Add( + ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate, + attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), + attributeFilePath, + mapPath(referenceResolver, unmappedMatch)); + return; } - } - if (matchingTree == null) - { - var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath.EndsWith(filePath), filePath); + // we might get: '\_\Program.cs' <-- slashes not normalized + // we might get: '\_/Program.cs' <-- slashes don't match + // we might get: 'Program.cs' <-- suffix match + // Force normalization of all '\' to '/', but when we recommend a path in the diagnostic message, ensure it will match what we expect if the user decides to use it. + var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, pair) + => mapPath(pair.referenceResolver, tree) + .Replace('\\', '/') + .EndsWith(pair.attributeFilePath), + (referenceResolver, attributeFilePath: attributeFilePath.Replace('\\', '/'))); if (suffixMatch != null) { - diagnostics.Add(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), filePath, suffixMatch.FilePath); - } - else - { - diagnostics.Add(ErrorCode.ERR_InterceptorPathNotInCompilation, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), filePath); + diagnostics.Add( + ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, + attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), + attributeFilePath, + mapPath(referenceResolver, suffixMatch)); + return; } + diagnostics.Add(ErrorCode.ERR_InterceptorPathNotInCompilation, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), attributeFilePath); + + return; + } + else if (matchingTrees.Count > 1) + { + diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeData.GetAttributeArgumentSyntaxLocation(filePathParameterIndex, attributeSyntax), attributeFilePath); return; } + 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; @@ -1079,7 +1091,12 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } - DeclaringCompilation.AddInterception(filePath, lineNumberZeroBased, characterNumberZeroBased, attributeLocation, this); + DeclaringCompilation.AddInterception(matchingTree.FilePath, lineNumberZeroBased, characterNumberZeroBased, attributeLocation, this); + + static string mapPath(SourceReferenceResolver? referenceResolver, SyntaxTree tree) + { + return referenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath; + } } private void DecodeUnmanagedCallersOnlyAttribute(ref DecodeWellKnownAttributeArguments arguments) diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 0f1f4c00da32e..7be2469288d8d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -847,11 +847,6 @@ Vlastnosti instance v rozhraních nemůžou mít inicializátory. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 952490ece3e92..c8e5655adca3c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -847,11 +847,6 @@ Instanzeigenschaften in Schnittstellen können keine Initialisierer aufweisen. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index bf3cc092fda87..5a0b77e2b904b 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -847,11 +847,6 @@ Las propiedades de la instancia en las interfaces no pueden tener inicializadores. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 42ee72dbe49a6..23db7e580c329 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -847,11 +847,6 @@ Les propriétés d'instance dans les interfaces ne peuvent pas avoir d'initialiseurs. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 4f76e002b5946..e4362aa55924e 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -847,11 +847,6 @@ Le proprietà di istanza nelle interfacce non possono avere inizializzatori. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index 85cb05f4332d8..ec86ff32e4232 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -847,11 +847,6 @@ インターフェイス内のインスタンス プロパティは初期化子を持つことができません。 - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index 7d40d808fff83..d442b56855029 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -847,11 +847,6 @@ 인터페이스의 인스턴스 속성은 이니셜라이저를 사용할 수 없습니다. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index 025fadbadcd6b..0671ad1e4c5a3 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -847,11 +847,6 @@ Właściwości wystąpienia w interfejsach nie mogą mieć inicjatorów. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 817089bfb458b..089e659d105d7 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -847,11 +847,6 @@ As propriedades da instância nas interfaces não podem ter inicializadores. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index c22c662868529..5f2e3fdb81831 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -847,11 +847,6 @@ Свойства экземпляра в интерфейсах не могут иметь инициализаторы. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 2c2fc249c8054..3915cc9716c0d 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -847,11 +847,6 @@ Arabirimlerdeki örnek özelliklerinin başlatıcıları olamaz. - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 9905cd20f5548..a15ab4d0f95d4 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -847,11 +847,6 @@ 接口中的实例属性不能具有初始值设定项。 - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index 938180a55d930..99eea9e6b277c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -847,11 +847,6 @@ 介面中的執行個體屬性不可有初始設定式。 - - An interceptable method must be an ordinary member method. - An interceptable method must be an ordinary member method. - - Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. Method '{0}' cannot be used as an interceptor because it or its containing type has type parameters. @@ -927,6 +922,11 @@ Cannot intercept: compilation does not contain a file with path '{0}'. Did you mean to use path '{1}'? + + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + Cannot intercept: Path '{0}' is unmapped. Expected mapped path '{1}'. + + The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. The provided line and character number does not refer to an interceptable method name, but rather to token '{0}'. diff --git a/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs b/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs index 27b99ac19c0b8..d2846aaf8a565 100644 --- a/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs +++ b/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs @@ -318,5 +318,73 @@ static void runTest(int n) }); } } + + [Fact] + public void Interceptors() + { + const int numberOfInterceptors = 10000; + + // write a program which has many intercepted calls. + // each interceptor is in a different file. + var files = ArrayBuilder<(string source, string path)>.GetInstance(); + + // Build a top-level-statements main like: + // C.M(); + // C.M(); + // C.M(); + // ... + var builder = new StringBuilder(); + for (int i = 0; i < numberOfInterceptors; i++) + { + builder.AppendLine("C.M();"); + } + + files.Add((builder.ToString(), "Program.cs")); + + files.Add((""" + class C + { + public static void M() => throw null!; + } + + namespace System.Runtime.CompilerServices + { + public class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string path, int line, int column) { } + } + } + """, "C.cs")); + + for (int i = 0; i < numberOfInterceptors; i++) + { + files.Add(($$""" + using System; + using System.Runtime.CompilerServices; + + class C{{i}} + { + [InterceptsLocation("Program.cs", {{i + 1}}, 3)] + public static void M() + { + Console.WriteLine({{i}}); + } + } + """, $"C{i}.cs")); + } + + var verifier = CompileAndVerify(files.ToArrayAndFree(), parseOptions: TestOptions.Regular.WithFeature("InterceptorsPreview"), expectedOutput: makeExpectedOutput()); + verifier.VerifyDiagnostics(); + + string makeExpectedOutput() + { + builder.Clear(); + for (int i = 0; i < numberOfInterceptors; i++) + { + builder.AppendLine($"{i}"); + } + return builder.ToString(); + } + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index 4377eee83fb77..ecd0e98fa763e 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -2,12 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; +using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; @@ -3420,4 +3424,479 @@ class C : I var verifier = CompileAndVerify(new[] { (source, "Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors, expectedOutput: "1"); verifier.VerifyDiagnostics(); } + + [Fact] + public void InterceptGetEnumerator() + { + var source = """ + using System.Collections; + using System.Runtime.CompilerServices; + + var myEnumerable = new MyEnumerable(); + foreach (var item in myEnumerable) + { + } + + class MyEnumerable : IEnumerable + { + public IEnumerator GetEnumerator() => throw null!; + } + + static class MyEnumerableExt + { + [InterceptsLocation("Program.cs", 5, 22)] // 1 + public static IEnumerator GetEnumerator1(this MyEnumerable en) => throw null!; + } + """; + + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // Program.cs(16,6): error CS27014: Possible method name 'myEnumerable' cannot be intercepted because it is not being invoked. + // [InterceptsLocation("Program.cs", 5, 22)] // 1 + Diagnostic(ErrorCode.ERR_InterceptorNameNotInvoked, @"InterceptsLocation(""Program.cs"", 5, 22)").WithArguments("myEnumerable").WithLocation(16, 6)); + } + + [Fact] + public void InterceptDispose() + { + var source = """ + using System; + using System.Runtime.CompilerServices; + + var myDisposable = new MyDisposable(); + using (myDisposable) + { + } + + class MyDisposable : IDisposable + { + public void Dispose() => throw null!; + } + + static class MyDisposeExt + { + [InterceptsLocation("Program.cs", 5, 8)] // 1 + public static void Dispose1(this MyDisposable md) => throw null!; + } + """; + + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // Program.cs(16,6): error CS27014: Possible method name 'myDisposable' cannot be intercepted because it is not being invoked. + // [InterceptsLocation("Program.cs", 5, 8)] // 1 + Diagnostic(ErrorCode.ERR_InterceptorNameNotInvoked, @"InterceptsLocation(""Program.cs"", 5, 8)").WithArguments("myDisposable").WithLocation(16, 6) + ); + } + + [Fact] + public void InterceptDeconstruct() + { + var source = """ + using System; + using System.Runtime.CompilerServices; + + var myDeconstructable = new MyDeconstructable(); + var (x, y) = myDeconstructable; + + class MyDeconstructable + { + public void Deconstruct(out int x, out int y) => throw null!; + } + + static class MyDeconstructableExt + { + [InterceptsLocation("Program.cs", 5, 14)] // 1 + public static void Deconstruct1(this MyDeconstructable md, out int x, out int y) => throw null!; + } + """; + + var comp = CreateCompilation(new[] { (source, "Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // Program.cs(14,6): error CS27014: Possible method name 'myDeconstructable' cannot be intercepted because it is not being invoked. + // [InterceptsLocation("Program.cs", 5, 14)] // 1 + Diagnostic(ErrorCode.ERR_InterceptorNameNotInvoked, @"InterceptsLocation(""Program.cs"", 5, 14)").WithArguments("myDeconstructable").WithLocation(14, 6) + ); + } + + [Fact] + public void PathMapping_01() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + + [InterceptsLocation("/_/Program.cs", 5, 3)] + public void Interceptor() => Console.Write(1); + } + """; + var pathPrefix = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path\""" : "/My/Machine/Specific/Path/"; + var path = pathPrefix + "Program.cs"; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, "/_/")); + + var verifier = CompileAndVerify( + new[] { (source, path), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap)), + expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void PathMapping_02() + { + // Attribute uses a physical path, but we expected a mapped path. + // Diagnostic can suggest using the mapped path instead. + var pathPrefix = PlatformInformation.IsWindows ? @"C:\My\Machine\Specific\Path\" : "/My/Machine/Specific/Path/"; + var path = pathPrefix + "Program.cs"; + var source = $$""" + using System.Runtime.CompilerServices; + using System; + + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + + [InterceptsLocation(@"{{path}}", 5, 3)] + public void Interceptor() => Console.Write(1); + } + """; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, "/_/")); + + var comp = CreateCompilation( + new[] { (source, path), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); + comp.VerifyEmitDiagnostics( + // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS27008: Cannot intercept: Path 'C:\My\Machine\Specific\Path\Program.cs' is unmapped. Expected mapped path '/_/Program.cs'. + // [InterceptsLocation(@"C:\My\Machine\Specific\Path\Program.cs", 5, 3)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate, $@"@""{path}""").WithArguments(path, "/_/Program.cs").WithLocation(11, 25) + ); + } + + [Fact] + public void PathMapping_03() + { + var source = """ + using System.Runtime.CompilerServices; + using System; + + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + + [InterceptsLocation(@"\_\Program.cs", 5, 3)] + public void Interceptor() => Console.Write(1); + } + """; + var pathPrefix = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path\""" : "/My/Machine/Specific/Path/"; + var path = pathPrefix + "Program.cs"; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, "/_/")); + + var comp = CreateCompilation( + new[] { (source, path), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); + comp.VerifyEmitDiagnostics( + // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS27003: Cannot intercept: compilation does not contain a file with path '\_\Program.cs'. Did you mean to use path '/_/Program.cs'? + // [InterceptsLocation(@"\_\Program.cs", 5, 3)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"@""\_\Program.cs""").WithArguments(@"\_\Program.cs", "/_/Program.cs").WithLocation(11, 25)); + } + + [Fact] + public void PathMapping_04() + { + // Test when unmapped file paths are distinct, but mapped paths are equal. + var source1 = """ + using System.Runtime.CompilerServices; + using System; + + namespace NS1; + + class C + { + public static void M0() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + + [InterceptsLocation(@"/_/Program.cs", 11, 9)] + public void Interceptor() => Console.Write(1); + } + """; + + var source2 = """ + using System.Runtime.CompilerServices; + using System; + + namespace NS2; + + class C + { + public static void M0() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + } + """; + + var pathPrefix1 = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path1\""" : "/My/Machine/Specific/Path1/"; + var pathPrefix2 = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path2\""" : "/My/Machine/Specific/Path2/"; + var path1 = pathPrefix1 + "Program.cs"; + var path2 = pathPrefix2 + "Program.cs"; + var pathMap = ImmutableArray.Create( + new KeyValuePair(pathPrefix1, "/_/"), + new KeyValuePair(pathPrefix2, "/_/") + ); + + var comp = CreateCompilation( + new[] { (source1, path1), (source2, path2), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugDll.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); + comp.VerifyEmitDiagnostics( + // C:\My\Machine\Specific\Path1\Program.cs(16,25): error CS27015: Cannot intercept a call in file with path '/_/Program.cs' because multiple files in the compilation have this path. + // [InterceptsLocation(@"/_/Program.cs", 11, 9)] + Diagnostic(ErrorCode.ERR_InterceptorNonUniquePath, @"@""/_/Program.cs""").WithArguments("/_/Program.cs").WithLocation(16, 25)); + } + + [Fact] + public void PathMapping_05() + { + // Pathmap replacement contains backslashes, and attribute path contains backslashes. + var source = """ + using System.Runtime.CompilerServices; + using System; + + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + + [InterceptsLocation(@"\_\Program.cs", 5, 3)] + public void Interceptor() => Console.Write(1); + } + """; + var pathPrefix = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path\""" : "/My/Machine/Specific/Path/"; + var path = pathPrefix + "Program.cs"; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, @"\_\")); + + var verifier = CompileAndVerify( + new[] { (source, path), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap)), + expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void PathMapping_06() + { + // Pathmap mixes slashes and backslashes, attribute path is normalized to slashes + var source = """ + using System.Runtime.CompilerServices; + using System; + + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + + [InterceptsLocation(@"/_/Program.cs", 5, 3)] + public void Interceptor() => Console.Write(1); + } + """; + var pathPrefix = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path\""" : "/My/Machine/Specific/Path/"; + var path = pathPrefix + "Program.cs"; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, @"\_/")); + + var comp = CreateCompilation( + new[] { (source, path), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); + comp.VerifyEmitDiagnostics( + // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS27003: Cannot intercept: compilation does not contain a file with path '/_/Program.cs'. Did you mean to use path '\_/Program.cs'? + // [InterceptsLocation(@"/_/Program.cs", 5, 3)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"@""/_/Program.cs""").WithArguments("/_/Program.cs", @"\_/Program.cs").WithLocation(11, 25)); + } + + [Fact] + public void PathMapping_07() + { + // Pathmap replacement mixes slashes and backslashes, attribute path matches it + var source = """ + using System.Runtime.CompilerServices; + using System; + + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + + [InterceptsLocation(@"\_/Program.cs", 5, 3)] + public void Interceptor() => Console.Write(1); + } + """; + var pathPrefix = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path\""" : "/My/Machine/Specific/Path/"; + var path = pathPrefix + "Program.cs"; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, @"\_/")); + + var verifier = CompileAndVerify( + new[] { (source, path), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap)), + expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void PathNormalization_01() + { + // No pathmap is present and slashes in the attribute match the FilePath on the syntax tree. + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + + [InterceptsLocation("src/Program.cs", 9, 11)] + public void Interceptor() => Console.Write(1); + } + """; + + var verifier = CompileAndVerify( + new[] { (source, "src/Program.cs"), s_attributesSource }, + parseOptions: RegularWithInterceptors, + expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void PathNormalization_02() + { + // No pathmap is present and backslashes in the attribute match the FilePath on the syntax tree. + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + + [InterceptsLocation(@"src\Program.cs", 9, 11)] + public void Interceptor() => Console.Write(1); + } + """; + + var verifier = CompileAndVerify( + new[] { (source, @"src\Program.cs"), s_attributesSource }, + parseOptions: RegularWithInterceptors, + expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void PathNormalization_03() + { + // Relative paths do not have slashes normalized when pathmap is not present + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + + [InterceptsLocation(@"src/Program.cs", 9, 11)] + public void Interceptor() => Console.Write(1); + } + """; + + var comp = CreateCompilation(new[] { (source, @"src\Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // src\Program.cs(14,25): error CS27003: Cannot intercept: compilation does not contain a file with path 'src/Program.cs'. Did you mean to use path 'src\Program.cs'? + // [InterceptsLocation(@"src/Program.cs", 9, 11)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"@""src/Program.cs""").WithArguments("src/Program.cs", @"src\Program.cs").WithLocation(14, 25)); + } + + [Fact] + public void PathNormalization_04() + { + // Absolute paths do not have slashes normalized when no pathmap is present + // Note that any such normalization step would be specific to Windows + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + + [InterceptsLocation("C:/src/Program.cs", 9, 11)] // 1 + public void Interceptor() => Console.Write(1); + } + """; + + var comp = CreateCompilation(new[] { (source, @"C:\src\Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // C:\src\Program.cs(14,25): error CS27003: Cannot intercept: compilation does not contain a file with path 'C:/src/Program.cs'. Did you mean to use path 'C:\src\Program.cs'? + // [InterceptsLocation("C:/src/Program.cs", 9, 11)] // 1 + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"""C:/src/Program.cs""").WithArguments("C:/src/Program.cs", @"C:\src\Program.cs").WithLocation(14, 25)); + } }