diff --git a/src/libraries/Common/src/Roslyn/CSharpSyntaxHelper.cs b/src/libraries/Common/src/Roslyn/CSharpSyntaxHelper.cs new file mode 100644 index 00000000000000..349d5f973c9149 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/CSharpSyntaxHelper.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions +{ + internal sealed class CSharpSyntaxHelper : AbstractSyntaxHelper + { + public static readonly ISyntaxHelper Instance = new CSharpSyntaxHelper(); + + private CSharpSyntaxHelper() + { + } + + public override bool IsCaseSensitive + => true; + + public override bool IsValidIdentifier(string name) + => SyntaxFacts.IsValidIdentifier(name); + + public override bool IsAnyNamespaceBlock(SyntaxNode node) + => node is BaseNamespaceDeclarationSyntax; + + public override bool IsAttribute(SyntaxNode node) + => node is AttributeSyntax; + + public override SyntaxNode GetNameOfAttribute(SyntaxNode node) + => ((AttributeSyntax)node).Name; + + public override bool IsAttributeList(SyntaxNode node) + => node is AttributeListSyntax; + + public override void AddAttributeTargets(SyntaxNode node, ref ValueListBuilder targets) + { + var attributeList = (AttributeListSyntax)node; + var container = attributeList.Parent; + Debug.Assert(container != null); + + // For fields/events, the attribute applies to all the variables declared. + if (container is FieldDeclarationSyntax field) + { + foreach (var variable in field.Declaration.Variables) + targets.Append(variable); + } + else if (container is EventFieldDeclarationSyntax ev) + { + foreach (var variable in ev.Declaration.Variables) + targets.Append(variable); + } + else + { + targets.Append(container); + } + } + + public override SeparatedSyntaxList GetAttributesOfAttributeList(SyntaxNode node) + => ((AttributeListSyntax)node).Attributes; + + public override bool IsLambdaExpression(SyntaxNode node) + => node is LambdaExpressionSyntax; + + public override SyntaxToken GetUnqualifiedIdentifierOfName(SyntaxNode node) + => ((NameSyntax)node).GetUnqualifiedName().Identifier; + + public override void AddAliases(SyntaxNode node, ref ValueListBuilder<(string aliasName, string symbolName)> aliases, bool global) + { + if (node is CompilationUnitSyntax compilationUnit) + { + AddAliases(compilationUnit.Usings, ref aliases, global); + } + else if (node is BaseNamespaceDeclarationSyntax namespaceDeclaration) + { + AddAliases(namespaceDeclaration.Usings, ref aliases, global); + } + else + { + Debug.Fail("This should not be reachable. Caller already checked we had a compilation unit or namespace."); + } + } + + private static void AddAliases(SyntaxList usings, ref ValueListBuilder<(string aliasName, string symbolName)> aliases, bool global) + { + foreach (var usingDirective in usings) + { + if (usingDirective.Alias is null) + continue; + + if (global != usingDirective.GlobalKeyword.Kind() is SyntaxKind.GlobalKeyword) + continue; + + var aliasName = usingDirective.Alias.Name.Identifier.ValueText; + var symbolName = usingDirective.Name.GetUnqualifiedName().Identifier.ValueText; + aliases.Append((aliasName, symbolName)); + } + } + + public override void AddAliases(CompilationOptions compilation, ref ValueListBuilder<(string aliasName, string symbolName)> aliases) + { + // C# doesn't have global aliases at the compilation level. + return; + } + } +} diff --git a/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs b/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs index c86d7f2e00ebc4..92728794020a2e 100644 --- a/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs +++ b/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Immutable; + using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions { @@ -134,5 +138,40 @@ private enum SymbolVisibility Private = 2, Friend = Internal, } + + internal static bool HasAttributeSuffix(this string name, bool isCaseSensitive) + { + const string AttributeSuffix = "Attribute"; + + var comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + return name.Length > AttributeSuffix.Length && name.EndsWith(AttributeSuffix, comparison); + } + + public static ImmutableArray ToImmutableArray(this ReadOnlySpan span) + { + switch (span.Length) + { + case 0: return ImmutableArray.Empty; + case 1: return ImmutableArray.Create(span[0]); + case 2: return ImmutableArray.Create(span[0], span[1]); + case 3: return ImmutableArray.Create(span[0], span[1], span[2]); + case 4: return ImmutableArray.Create(span[0], span[1], span[2], span[3]); + default: + var builder = ImmutableArray.CreateBuilder(span.Length); + foreach (var item in span) + builder.Add(item); + + return builder.MoveToImmutable(); + } + } + + public static SimpleNameSyntax GetUnqualifiedName(this NameSyntax name) + => name switch + { + AliasQualifiedNameSyntax alias => alias.Name, + QualifiedNameSyntax qualified => qualified.Right, + SimpleNameSyntax simple => simple, + _ => throw new InvalidOperationException("Unreachable"), + }; } } diff --git a/src/libraries/Common/src/Roslyn/GlobalAliases.cs b/src/libraries/Common/src/Roslyn/GlobalAliases.cs new file mode 100644 index 00000000000000..2108923c76c271 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/GlobalAliases.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions; + +/// +/// Simple wrapper class around an immutable array so we can have the value-semantics needed for the incremental +/// generator to know when a change actually happened and it should run later transform stages. +/// +internal sealed class GlobalAliases : IEquatable +{ + public static readonly GlobalAliases Empty = new(ImmutableArray<(string aliasName, string symbolName)>.Empty); + + public readonly ImmutableArray<(string aliasName, string symbolName)> AliasAndSymbolNames; + + private int _hashCode; + + private GlobalAliases(ImmutableArray<(string aliasName, string symbolName)> aliasAndSymbolNames) + { + AliasAndSymbolNames = aliasAndSymbolNames; + } + + public static GlobalAliases Create(ImmutableArray<(string aliasName, string symbolName)> aliasAndSymbolNames) + { + return aliasAndSymbolNames.IsEmpty ? Empty : new GlobalAliases(aliasAndSymbolNames); + } + + public static GlobalAliases Concat(GlobalAliases ga1, GlobalAliases ga2) + { + if (ga1.AliasAndSymbolNames.Length == 0) + return ga2; + + if (ga2.AliasAndSymbolNames.Length == 0) + return ga1; + + return new(ga1.AliasAndSymbolNames.AddRange(ga2.AliasAndSymbolNames)); + } + + public override int GetHashCode() + { + if (_hashCode == 0) + { + var hashCode = 0; + foreach (var tuple in this.AliasAndSymbolNames) + hashCode = Hash.Combine(tuple.GetHashCode(), hashCode); + + _hashCode = hashCode == 0 ? 1 : hashCode; + } + + return _hashCode; + } + + public override bool Equals(object? obj) + => this.Equals(obj as GlobalAliases); + + public bool Equals(GlobalAliases? aliases) + { + if (aliases is null) + return false; + + if (ReferenceEquals(this, aliases)) + return true; + + if (this.AliasAndSymbolNames == aliases.AliasAndSymbolNames) + return true; + + return this.AliasAndSymbolNames.AsSpan().SequenceEqual(aliases.AliasAndSymbolNames.AsSpan()); + } +} diff --git a/src/libraries/Common/src/Roslyn/Hash.cs b/src/libraries/Common/src/Roslyn/Hash.cs new file mode 100644 index 00000000000000..028ac50017e0b9 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/Hash.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Roslyn.Utilities +{ + internal static class Hash + { + /// + /// This is how VB Anonymous Types combine hash values for fields. + /// + internal static int Combine(int newKey, int currentKey) + { + return unchecked((currentKey * (int)0xA5555529) + newKey); + } + + // The rest of this file was removed as they were not currently needed in the polyfill of SyntaxValueProvider.ForAttributeWithMetadataName. + // If that changes, they should be added back as necessary. + } +} diff --git a/src/libraries/Common/src/Roslyn/ISyntaxHelper.cs b/src/libraries/Common/src/Roslyn/ISyntaxHelper.cs new file mode 100644 index 00000000000000..95bf4e72ecfa84 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/ISyntaxHelper.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions +{ + internal interface ISyntaxHelper + { + bool IsCaseSensitive { get; } + + bool IsValidIdentifier(string name); + + bool IsAnyNamespaceBlock(SyntaxNode node); + + bool IsAttributeList(SyntaxNode node); + SeparatedSyntaxList GetAttributesOfAttributeList(SyntaxNode node); + + void AddAttributeTargets(SyntaxNode node, ref ValueListBuilder targets); + + bool IsAttribute(SyntaxNode node); + SyntaxNode GetNameOfAttribute(SyntaxNode node); + + bool IsLambdaExpression(SyntaxNode node); + + SyntaxToken GetUnqualifiedIdentifierOfName(SyntaxNode node); + + /// + /// must be a compilation unit or namespace block. + /// + void AddAliases(SyntaxNode node, ref ValueListBuilder<(string aliasName, string symbolName)> aliases, bool global); + void AddAliases(CompilationOptions options, ref ValueListBuilder<(string aliasName, string symbolName)> aliases); + } + + internal abstract class AbstractSyntaxHelper : ISyntaxHelper + { + public abstract bool IsCaseSensitive { get; } + + public abstract bool IsValidIdentifier(string name); + + public abstract SyntaxToken GetUnqualifiedIdentifierOfName(SyntaxNode name); + + public abstract bool IsAnyNamespaceBlock(SyntaxNode node); + + public abstract bool IsAttribute(SyntaxNode node); + public abstract SyntaxNode GetNameOfAttribute(SyntaxNode node); + + public abstract bool IsAttributeList(SyntaxNode node); + public abstract SeparatedSyntaxList GetAttributesOfAttributeList(SyntaxNode node); + public abstract void AddAttributeTargets(SyntaxNode node, ref ValueListBuilder targets); + + public abstract bool IsLambdaExpression(SyntaxNode node); + + public abstract void AddAliases(SyntaxNode node, ref ValueListBuilder<(string aliasName, string symbolName)> aliases, bool global); + public abstract void AddAliases(CompilationOptions options, ref ValueListBuilder<(string aliasName, string symbolName)> aliases); + } +} diff --git a/src/libraries/Common/src/Roslyn/SyntaxNodeGrouping.cs b/src/libraries/Common/src/Roslyn/SyntaxNodeGrouping.cs new file mode 100644 index 00000000000000..94dd5ae5f9d13c --- /dev/null +++ b/src/libraries/Common/src/Roslyn/SyntaxNodeGrouping.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions; + +internal static partial class SyntaxValueProviderExtensions +{ + /// + /// Wraps a grouping of nodes within a syntax tree so we can have value-semantics around them usable by the + /// incremental driver. Note: we do something very sneaky here. Specifically, as long as we have the same from before, then we know we must have the same nodes as before (since the nodes are + /// entirely determined from the text+options which is exactly what the syntax tree represents). Similarly, if the + /// syntax tree changes, we will always get different nodes (since they point back at the syntax tree). So we can + /// just use the syntax tree itself to determine value semantics here. + /// + private sealed class SyntaxNodeGrouping : IEquatable> + where TSyntaxNode : SyntaxNode + { + public readonly SyntaxTree SyntaxTree; + public readonly ImmutableArray SyntaxNodes; + + public SyntaxNodeGrouping(IGrouping grouping) + { + SyntaxTree = grouping.Key; + SyntaxNodes = grouping.OrderBy(static n => n.FullSpan.Start).ToImmutableArray(); + } + + public override int GetHashCode() + => SyntaxTree.GetHashCode(); + + public override bool Equals(object? obj) + => Equals(obj as SyntaxNodeGrouping); + + public bool Equals(SyntaxNodeGrouping? obj) + => this.SyntaxTree == obj?.SyntaxTree; + } +} diff --git a/src/libraries/Common/src/Roslyn/SyntaxValueProvider.ImmutableArrayValueComparer.cs b/src/libraries/Common/src/Roslyn/SyntaxValueProvider.ImmutableArrayValueComparer.cs new file mode 100644 index 00000000000000..5323a100883a73 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/SyntaxValueProvider.ImmutableArrayValueComparer.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions; + +internal static partial class SyntaxValueProviderExtensions +{ + private sealed class ImmutableArrayValueComparer : IEqualityComparer> + { + public static readonly IEqualityComparer> Instance = new ImmutableArrayValueComparer(); + + public bool Equals(ImmutableArray x, ImmutableArray y) + => x.SequenceEqual(y, EqualityComparer.Default); + + public int GetHashCode(ImmutableArray obj) + { + var hashCode = 0; + foreach (var value in obj) + hashCode = Hash.Combine(hashCode, EqualityComparer.Default.GetHashCode(value!)); + + return hashCode; + } + } +} diff --git a/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithMetadataName.cs b/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithMetadataName.cs new file mode 100644 index 00000000000000..86c75926842cd0 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithMetadataName.cs @@ -0,0 +1,211 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +using Microsoft.CodeAnalysis; + +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions; + +internal readonly struct GeneratorAttributeSyntaxContext +{ + /// + /// The syntax node the attribute is attached to. For example, with [CLSCompliant] class C { } this would + /// the class declaration node. + /// + public SyntaxNode TargetNode { get; } + + /// + /// The symbol that the attribute is attached to. For example, with [CLSCompliant] class C { } this would be + /// the for "C". + /// + public ISymbol TargetSymbol { get; } + + /// + /// Semantic model for the file that is contained within. + /// + public SemanticModel SemanticModel { get; } + + /// + /// s for any matching attributes on . Always non-empty. All + /// these attributes will have an whose fully qualified name metadata + /// name matches the name requested in . + /// + /// To get the entire list of attributes, use on . + /// + /// + public ImmutableArray Attributes { get; } + + internal GeneratorAttributeSyntaxContext( + SyntaxNode targetNode, + ISymbol targetSymbol, + SemanticModel semanticModel, + ImmutableArray attributes) + { + TargetNode = targetNode; + TargetSymbol = targetSymbol; + SemanticModel = semanticModel; + Attributes = attributes; + } +} + +internal static partial class SyntaxValueProviderExtensions +{ +#if false + + // Deviation from roslyn. We do not support attributes that are nested or generic. That's ok as that's not a + // scenario that ever arises in our generators. + + private static readonly char[] s_nestedTypeNameSeparators = new char[] { '+' }; + + private static readonly SymbolDisplayFormat s_metadataDisplayFormat = + SymbolDisplayFormat.QualifiedNameArityFormat.AddCompilerInternalOptions(SymbolDisplayCompilerInternalOptions.UsePlusForNestedTypes); + +#endif + + /// + /// Creates an that can provide a transform over all s if that node has an attribute on it that binds to a with the + /// same fully-qualified metadata as the provided . should be the fully-qualified, metadata name of the attribute, including the + /// Attribute suffix. For example "System.CLSCompliantAttribute for . + /// + /// A function that determines if the given attribute target () should be transformed. Nodes that do not pass this + /// predicate will not have their attributes looked at at all. + /// A function that performs the transform. This will only be passed nodes that return for and which have a matching whose + /// has the same fully qualified, metadata name as . + public static IncrementalValuesProvider ForAttributeWithMetadataName( + this SyntaxValueProvider @this, + IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName, + Func predicate, + Func transform) + { +#if false + + // Deviation from roslyn. We do not support attributes that are nested or generic. That's ok as that's not a + // scenario that ever arises in our generators. + + var metadataName = fullyQualifiedMetadataName.Contains('+') + ? MetadataTypeName.FromFullName(fullyQualifiedMetadataName.Split(s_nestedTypeNameSeparators).Last()) + : MetadataTypeName.FromFullName(fullyQualifiedMetadataName); + + var nodesWithAttributesMatchingSimpleName = @this.ForAttributeWithSimpleName(context, metadataName.UnmangledTypeName, predicate); + +#else + + var lastDotIndex = fullyQualifiedMetadataName.LastIndexOf('.'); + Debug.Assert(lastDotIndex > 0); + var unmangledTypeName = fullyQualifiedMetadataName.Substring(lastDotIndex + 1); + + var nodesWithAttributesMatchingSimpleName = @this.ForAttributeWithSimpleName(context, unmangledTypeName, predicate); + +#endif + + var collectedNodes = nodesWithAttributesMatchingSimpleName + .Collect() + .WithComparer(ImmutableArrayValueComparer.Instance) + /*.WithTrackingName("collectedNodes_ForAttributeWithMetadataName")*/; + + // Group all the nodes by syntax tree, so we can process a whole syntax tree at a time. This will let us make + // the required semantic model for it once, instead of potentially many times (in the rare, but possible case of + // a single file with a ton of matching nodes in it). + var groupedNodes = collectedNodes.SelectMany( + static (array, cancellationToken) => + array.GroupBy(static n => n.SyntaxTree) + .Select(static g => new SyntaxNodeGrouping(g)))/*.WithTrackingName("groupedNodes_ForAttributeWithMetadataName")*/; + + var compilationAndGroupedNodesProvider = groupedNodes + .Combine(context.CompilationProvider) + /*.WithTrackingName("compilationAndGroupedNodes_ForAttributeWithMetadataName")*/; + + var syntaxHelper = CSharpSyntaxHelper.Instance; + var finalProvider = compilationAndGroupedNodesProvider.SelectMany((tuple, cancellationToken) => + { + var (grouping, compilation) = tuple; + + using var result = new ValueListBuilder(Span.Empty); + var syntaxTree = grouping.SyntaxTree; + var semanticModel = compilation.GetSemanticModel(syntaxTree); + + foreach (var targetNode in grouping.SyntaxNodes) + { + cancellationToken.ThrowIfCancellationRequested(); + + var targetSymbol = + targetNode is ICompilationUnitSyntax compilationUnit ? semanticModel.Compilation.Assembly : + syntaxHelper.IsLambdaExpression(targetNode) ? semanticModel.GetSymbolInfo(targetNode, cancellationToken).Symbol : + semanticModel.GetDeclaredSymbol(targetNode, cancellationToken); + if (targetSymbol is null) + continue; + + var attributes = getMatchingAttributes(targetNode, targetSymbol, fullyQualifiedMetadataName); + if (attributes.Length > 0) + { + result.Append(transform( + new GeneratorAttributeSyntaxContext(targetNode, targetSymbol, semanticModel, attributes), + cancellationToken)); + } + } + + return result.AsSpan().ToImmutableArray(); + })/*.WithTrackingName("result_ForAttributeWithMetadataName")*/; + + return finalProvider; + + static ImmutableArray getMatchingAttributes( + SyntaxNode attributeTarget, + ISymbol symbol, + string fullyQualifiedMetadataName) + { + var targetSyntaxTree = attributeTarget.SyntaxTree; + var result = new ValueListBuilder(Span.Empty); + + try + { + addMatchingAttributes(ref result, symbol.GetAttributes()); + addMatchingAttributes(ref result, (symbol as IMethodSymbol)?.GetReturnTypeAttributes()); + + if (symbol is IAssemblySymbol assemblySymbol) + { + foreach (var module in assemblySymbol.Modules) + addMatchingAttributes(ref result, module.GetAttributes()); + } + + return result.AsSpan().ToImmutableArray(); + } + finally + { + result.Dispose(); + } + + void addMatchingAttributes( + ref ValueListBuilder result, + ImmutableArray? attributes) + { + if (!attributes.HasValue) + return; + + foreach (var attribute in attributes.Value) + { + if (attribute.ApplicationSyntaxReference?.SyntaxTree == targetSyntaxTree && + attribute.AttributeClass?.ToDisplayString(/*s_metadataDisplayFormat*/) == fullyQualifiedMetadataName) + { + result.Append(attribute); + } + } + } + } + } +} diff --git a/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithSimpleName.cs b/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithSimpleName.cs new file mode 100644 index 00000000000000..e040b4968be598 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithSimpleName.cs @@ -0,0 +1,309 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +using Microsoft.CodeAnalysis; + +using Aliases = System.Collections.Generic.ValueListBuilder<(string aliasName, string symbolName)>; + +namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions; + +internal static partial class SyntaxValueProviderExtensions +{ + // private static readonly ObjectPool> s_stackPool = new(static () => new()); + + /// + /// Returns all syntax nodes of that match if that node has an attribute on it that + /// could possibly bind to the provided . should be the + /// simple, non-qualified, name of the attribute, including the Attribute suffix, and not containing any + /// generics, containing types, or namespaces. For example CLSCompliantAttribute for . + /// This provider understands (Import in Visual Basic) aliases and will find + /// matches even when the attribute references an alias name. For example, given: + /// + /// using XAttribute = System.CLSCompliantAttribute; + /// [X] + /// class C { } + /// + /// Then + /// context.SyntaxProvider.CreateSyntaxProviderForAttribute(nameof(CLSCompliantAttribute), (node, c) => node is ClassDeclarationSyntax) + /// will find the C class. + /// + public static IncrementalValuesProvider ForAttributeWithSimpleName( + this SyntaxValueProvider @this, + IncrementalGeneratorInitializationContext context, + string simpleName, + Func predicate) + { + var syntaxHelper = CSharpSyntaxHelper.Instance; + + // Create a provider over all the syntax trees in the compilation. This is better than CreateSyntaxProvider as + // using SyntaxTrees is purely syntax and will not update the incremental node for a tree when another tree is + // changed. CreateSyntaxProvider will have to rerun all incremental nodes since it passes along the + // SemanticModel, and that model is updated whenever any tree changes (since it is tied to the compilation). + var syntaxTreesProvider = context.CompilationProvider + .SelectMany(static (c, _) => c.SyntaxTrees) + /*.WithTrackingName("compilationUnit_ForAttribute")*/; + + // Create a provider that provides (and updates) the global aliases for any particular file when it is edited. + var individualFileGlobalAliasesProvider = syntaxTreesProvider.Select( + (s, c) => getGlobalAliasesInCompilationUnit(s.GetRoot(c)))/*.WithTrackingName("individualFileGlobalAliases_ForAttribute")*/; + + // Create an aggregated view of all global aliases across all files. This should only update when an individual + // file changes its global aliases or a file is added / removed from the compilation + var collectedGlobalAliasesProvider = individualFileGlobalAliasesProvider + .Collect() + .WithComparer(ImmutableArrayValueComparer.Instance) + /*.WithTrackingName("collectedGlobalAliases_ForAttribute")*/; + + var allUpGlobalAliasesProvider = collectedGlobalAliasesProvider + .Select(static (arrays, _) => GlobalAliases.Create(arrays.SelectMany(a => a.AliasAndSymbolNames).ToImmutableArray())) + /*.WithTrackingName("allUpGlobalAliases_ForAttribute")*/; + +#if false + + // C# does not support global aliases from compilation options. So we can just ignore this part. + + // Regenerate our data if the compilation options changed. VB can supply global aliases with compilation options, + // so we have to reanalyze everything if those changed. + var compilationGlobalAliases = _context.CompilationOptionsProvider.Select( + (o, _) => + { + var aliases = Aliases.GetInstance(); + syntaxHelper.AddAliases(o, aliases); + return GlobalAliases.Create(aliases.ToImmutableAndFree()); + }).WithTrackingName("compilationGlobalAliases_ForAttribute"); + + allUpGlobalAliasesProvider = allUpGlobalAliasesProvider + .Combine(compilationGlobalAliases) + .Select((tuple, _) => GlobalAliases.Concat(tuple.Left, tuple.Right)) + .WithTrackingName("allUpIncludingCompilationGlobalAliases_ForAttribute"); + +#endif + + // Combine the two providers so that we reanalyze every file if the global aliases change, or we reanalyze a + // particular file when it's compilation unit changes. + var syntaxTreeAndGlobalAliasesProvider = syntaxTreesProvider + .Combine(allUpGlobalAliasesProvider) + /*.WithTrackingName("compilationUnitAndGlobalAliases_ForAttribute")*/; + + // For each pair of compilation unit + global aliases, walk the compilation unit + var result = syntaxTreeAndGlobalAliasesProvider + .SelectMany((globalAliasesAndCompilationUnit, cancellationToken) => GetMatchingNodes( + syntaxHelper, globalAliasesAndCompilationUnit.Right, globalAliasesAndCompilationUnit.Left, simpleName, predicate, cancellationToken)) + /*.WithTrackingName("result_ForAttribute")*/; + + return result; + + static GlobalAliases getGlobalAliasesInCompilationUnit( + SyntaxNode compilationUnit) + { + Debug.Assert(compilationUnit is ICompilationUnitSyntax); + var globalAliases = new Aliases(Span<(string aliasName, string symbolName)>.Empty); + + CSharpSyntaxHelper.Instance.AddAliases(compilationUnit, ref globalAliases, global: true); + + return GlobalAliases.Create(globalAliases.AsSpan().ToImmutableArray()); + } + } + + private static ImmutableArray GetMatchingNodes( + ISyntaxHelper syntaxHelper, + GlobalAliases globalAliases, + SyntaxTree syntaxTree, + string name, + Func predicate, + CancellationToken cancellationToken) + { + var compilationUnit = syntaxTree.GetRoot(cancellationToken); + Debug.Assert(compilationUnit is ICompilationUnitSyntax); + + var isCaseSensitive = syntaxHelper.IsCaseSensitive; + var comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + // As we walk down the compilation unit and nested namespaces, we may encounter additional using aliases local + // to this file. Keep track of them so we can determine if they would allow an attribute in code to bind to the + // attribute being searched for. + var localAliases = new Aliases(Span<(string, string)>.Empty); + var nameHasAttributeSuffix = name.HasAttributeSuffix(isCaseSensitive); + + // Used to ensure that as we recurse through alias names to see if they could bind to attributeName that we + // don't get into cycles. + + var seenNames = new ValueListBuilder(Span.Empty); + var results = new ValueListBuilder(Span.Empty); + var attributeTargets = new ValueListBuilder(Span.Empty); + + try + { + recurse(compilationUnit, ref localAliases, ref seenNames, ref results, ref attributeTargets); + + if (results.Length == 0) + return ImmutableArray.Empty; + + return results.AsSpan().ToArray().Distinct().ToImmutableArray(); + } + finally + { + attributeTargets.Dispose(); + results.Dispose(); + seenNames.Dispose(); + } + + void recurse( + SyntaxNode node, + ref Aliases localAliases, + ref ValueListBuilder seenNames, + ref ValueListBuilder results, + ref ValueListBuilder attributeTargets) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (node is ICompilationUnitSyntax) + { + syntaxHelper.AddAliases(node, ref localAliases, global: false); + + recurseChildren(node, ref localAliases, ref seenNames, ref results, ref attributeTargets); + } + else if (syntaxHelper.IsAnyNamespaceBlock(node)) + { + var localAliasCount = localAliases.Length; + syntaxHelper.AddAliases(node, ref localAliases, global: false); + + recurseChildren(node, ref localAliases, ref seenNames, ref results, ref attributeTargets); + + // after recursing into this namespace, dump any local aliases we added from this namespace decl itself. + localAliases.Length = localAliasCount; + } + else if (syntaxHelper.IsAttributeList(node)) + { + foreach (var attribute in syntaxHelper.GetAttributesOfAttributeList(node)) + { + // Have to lookup both with the name in the attribute, as well as adding the 'Attribute' suffix. + // e.g. if there is [X] then we have to lookup with X and with XAttribute. + var simpleAttributeName = syntaxHelper.GetUnqualifiedIdentifierOfName( + syntaxHelper.GetNameOfAttribute(attribute)).ValueText; + if (matchesAttributeName(ref localAliases, ref seenNames, simpleAttributeName, withAttributeSuffix: false) || + matchesAttributeName(ref localAliases, ref seenNames, simpleAttributeName, withAttributeSuffix: true)) + { + attributeTargets.Length = 0; + syntaxHelper.AddAttributeTargets(node, ref attributeTargets); + + foreach (var target in attributeTargets.AsSpan()) + { + if (predicate(target, cancellationToken)) + results.Append(target); + } + + return; + } + } + + // attributes can't have attributes inside of them. so no need to recurse when we're done. + } + else + { + // For any other node, just keep recursing deeper to see if we can find an attribute. Note: we cannot + // terminate the search anywhere as attributes may be found on things like local functions, and that + // means having to dive deep into statements and expressions. + recurseChildren(node, ref localAliases, ref seenNames, ref results, ref attributeTargets); + } + + return; + + void recurseChildren( + SyntaxNode node, + ref Aliases localAliases, + ref ValueListBuilder seenNames, + ref ValueListBuilder results, + ref ValueListBuilder attributeTargets) + { + foreach (var child in node.ChildNodesAndTokens()) + { + if (child.IsNode) + recurse(child.AsNode()!, ref localAliases, ref seenNames, ref results, ref attributeTargets); + } + } + } + + // Checks if `name` is equal to `matchAgainst`. if `withAttributeSuffix` is true, then + // will check if `name` + "Attribute" is equal to `matchAgainst` + bool matchesName(string name, string matchAgainst, bool withAttributeSuffix) + { + if (withAttributeSuffix) + { + return name.Length + "Attribute".Length == matchAgainst.Length && + matchAgainst.HasAttributeSuffix(isCaseSensitive) && + matchAgainst.StartsWith(name, comparison); + } + else + { + return name.Equals(matchAgainst, comparison); + } + } + + bool matchesAttributeName( + ref Aliases localAliases, + ref ValueListBuilder seenNames, + string currentAttributeName, + bool withAttributeSuffix) + { + // If the names match, we're done. + if (withAttributeSuffix) + { + if (nameHasAttributeSuffix && + matchesName(currentAttributeName, name, withAttributeSuffix)) + { + return true; + } + } + else + { + if (matchesName(currentAttributeName, name, withAttributeSuffix: false)) + return true; + } + + // Otherwise, keep searching through aliases. Check that this is the first time seeing this name so we + // don't infinite recurse in error code where aliases reference each other. + // + // note: as we recurse up the aliases, we do not want to add the attribute suffix anymore. aliases must + // reference the actual real name of the symbol they are aliasing. + foreach (var seenName in seenNames.AsSpan()) + { + if (seenName == currentAttributeName) + return false; + } + + seenNames.Append(currentAttributeName); + + foreach (var (aliasName, symbolName) in localAliases.AsSpan()) + { + // see if user wrote `[SomeAlias]`. If so, if we find a `using SomeAlias = ...` recurse using the + // ... name portion to see if it might bind to the attr name the caller is searching for. + if (matchesName(currentAttributeName, aliasName, withAttributeSuffix) && + matchesAttributeName(ref localAliases, ref seenNames, symbolName, withAttributeSuffix: false)) + { + return true; + } + } + + foreach (var (aliasName, symbolName) in globalAliases.AliasAndSymbolNames) + { + if (matchesName(currentAttributeName, aliasName, withAttributeSuffix) && + matchesAttributeName(ref localAliases, ref seenNames, symbolName, withAttributeSuffix: false)) + { + return true; + } + } + + seenNames.Pop(); + return false; + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs index 9fa9110e23afe9..33dd18fc67f8fd 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs @@ -1,19 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; -using System; -using System.Collections; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; + namespace System.Text.RegularExpressions.Generator { public partial class RegexGenerator @@ -21,38 +18,13 @@ public partial class RegexGenerator private const string RegexName = "System.Text.RegularExpressions.Regex"; private const string RegexGeneratorAttributeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; - private static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken cancellationToken) => - // We don't have a semantic model here, so the best we can do is say whether there are any attributes. - node is MethodDeclarationSyntax { AttributeLists.Count: > 0 }; - - private static bool IsSemanticTargetForGeneration(SemanticModel semanticModel, MethodDeclarationSyntax methodDeclarationSyntax, CancellationToken cancellationToken) - { - foreach (AttributeListSyntax attributeListSyntax in methodDeclarationSyntax.AttributeLists) - { - foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) - { - if (semanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).Symbol is IMethodSymbol attributeSymbol && - attributeSymbol.ContainingType.ToDisplayString() == RegexGeneratorAttributeName) - { - return true; - } - } - } - - return false; - } - // Returns null if nothing to do, Diagnostic if there's an error to report, or RegexType if the type was analyzed successfully. - private static object? GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken cancellationToken) + private static object? GetSemanticTargetForGeneration( + GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { - var methodSyntax = (MethodDeclarationSyntax)context.Node; + var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; SemanticModel sm = context.SemanticModel; - if (!IsSemanticTargetForGeneration(sm, methodSyntax, cancellationToken)) - { - return null; - } - Compilation compilation = sm.Compilation; INamedTypeSymbol? regexSymbol = compilation.GetBestTypeByMetadataName(RegexName); INamedTypeSymbol? regexGeneratorAttributeSymbol = compilation.GetBestTypeByMetadataName(RegexGeneratorAttributeName); @@ -69,7 +41,7 @@ private static bool IsSemanticTargetForGeneration(SemanticModel semanticModel, M return null; } - IMethodSymbol? regexMethodSymbol = sm.GetDeclaredSymbol(methodSyntax, cancellationToken) as IMethodSymbol; + IMethodSymbol regexMethodSymbol = context.TargetSymbol as IMethodSymbol; if (regexMethodSymbol is null) { return null; diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs index 8aac6281fa0022..388984725f6a3a 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; [assembly: System.Resources.NeutralResourcesLanguage("en-us")] @@ -51,7 +52,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.SyntaxProvider // Find all MethodDeclarationSyntax nodes attributed with RegexGenerator and gather the required information. - .CreateSyntaxProvider(IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration) + .ForAttributeWithMetadataName( + context, + RegexGeneratorAttributeName, + (node, _) => node is MethodDeclarationSyntax, + GetSemanticTargetForGeneration) .Where(static m => m is not null) // Generate the RunnerFactory for each regex, if possible. This is where the bulk of the implementation occurs. diff --git a/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj b/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj index 082bc8f33c0fe9..ff5a56cee85696 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj +++ b/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj @@ -20,6 +20,14 @@ + + + + + + + +