-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Polyfill the incremental generator ForAttributeWithMetadataName from roslyn. #70911
Changes from all commits
6e03dca
539b248
e8f0a8e
1c0fbf2
b533f26
c1a76b1
2ef0717
1f35a7e
f5d8137
be4ef0f
2babd86
1cdc428
cfce553
fd9a27e
e2613b6
a0e9d88
91672fe
86de769
cc933a2
8a5a8d0
4eb00d1
59cdea7
e7a1f3e
20d62a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SyntaxNode> 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<SyntaxNode> 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<UsingDirectiveSyntax> 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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. port of roslyn helper. |
||
|
||
public static ImmutableArray<T> ToImmutableArray<T>(this ReadOnlySpan<T> span) | ||
{ | ||
switch (span.Length) | ||
{ | ||
case 0: return ImmutableArray<T>.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<T>(span.Length); | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
foreach (var item in span) | ||
builder.Add(item); | ||
|
||
return builder.MoveToImmutable(); | ||
} | ||
} | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public static SimpleNameSyntax GetUnqualifiedName(this NameSyntax name) | ||
=> name switch | ||
{ | ||
AliasQualifiedNameSyntax alias => alias.Name, | ||
QualifiedNameSyntax qualified => qualified.Right, | ||
SimpleNameSyntax simple => simple, | ||
_ => throw new InvalidOperationException("Unreachable"), | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. direct port with only necessary cleanup to fit into runtime. |
||
// 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; | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// 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. | ||
stephentoub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// </summary> | ||
internal sealed class GlobalAliases : IEquatable<GlobalAliases> | ||
{ | ||
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()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. direct port with only necessary cleanup to fit into runtime. |
||
// 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 | ||
{ | ||
/// <summary> | ||
/// This is how VB Anonymous Types combine hash values for fields. | ||
/// </summary> | ||
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. | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
bool IsCaseSensitive { get; } | ||
|
||
bool IsValidIdentifier(string name); | ||
|
||
bool IsAnyNamespaceBlock(SyntaxNode node); | ||
|
||
bool IsAttributeList(SyntaxNode node); | ||
SeparatedSyntaxList<SyntaxNode> GetAttributesOfAttributeList(SyntaxNode node); | ||
|
||
void AddAttributeTargets(SyntaxNode node, ref ValueListBuilder<SyntaxNode> targets); | ||
|
||
bool IsAttribute(SyntaxNode node); | ||
SyntaxNode GetNameOfAttribute(SyntaxNode node); | ||
|
||
bool IsLambdaExpression(SyntaxNode node); | ||
|
||
SyntaxToken GetUnqualifiedIdentifierOfName(SyntaxNode node); | ||
|
||
/// <summary> | ||
/// <paramref name="node"/> must be a compilation unit or namespace block. | ||
/// </summary> | ||
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<SyntaxNode> GetAttributesOfAttributeList(SyntaxNode node); | ||
public abstract void AddAttributeTargets(SyntaxNode node, ref ValueListBuilder<SyntaxNode> 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. copied wholesale with only the changes necessary for runtime. |
||
// 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 | ||
{ | ||
/// <summary> | ||
/// 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 <see | ||
/// cref="SyntaxTree"/> 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. | ||
/// </summary> | ||
private sealed class SyntaxNodeGrouping<TSyntaxNode> : IEquatable<SyntaxNodeGrouping<TSyntaxNode>> | ||
where TSyntaxNode : SyntaxNode | ||
{ | ||
public readonly SyntaxTree SyntaxTree; | ||
public readonly ImmutableArray<TSyntaxNode> SyntaxNodes; | ||
|
||
public SyntaxNodeGrouping(IGrouping<SyntaxTree, TSyntaxNode> 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<TSyntaxNode>); | ||
|
||
public bool Equals(SyntaxNodeGrouping<TSyntaxNode>? obj) | ||
=> this.SyntaxTree == obj?.SyntaxTree; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. copied wholesale with only the changes necessary for runtime. |
||
// 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<T> : IEqualityComparer<ImmutableArray<T>> | ||
{ | ||
public static readonly IEqualityComparer<ImmutableArray<T>> Instance = new ImmutableArrayValueComparer<T>(); | ||
|
||
public bool Equals(ImmutableArray<T> x, ImmutableArray<T> y) | ||
=> x.SequenceEqual(y, EqualityComparer<T>.Default); | ||
|
||
public int GetHashCode(ImmutableArray<T> obj) | ||
{ | ||
var hashCode = 0; | ||
foreach (var value in obj) | ||
hashCode = Hash.Combine(hashCode, EqualityComparer<T>.Default.GetHashCode(value!)); | ||
|
||
return hashCode; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the general approach i took was to copy the roslyn code whole-sale and make as few changes to it as possible. The only changes i made were:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These files are all then source-linked into the project that needs them. So subsequent updates to other generators will be much easier as they can just leverage this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to have someone from the Roslyn team review all files added under
Common/src/Roslyn
? I understand that this is a direct port with just minimal changes, but it may be worth it to have that extra pair of eyes over those minimal changes from the domain experts.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. @chsienki and @jaredpar both reviewed the roslyn side. They should likely review this port as well.