From f4d89087a2f09db91eb988411d675130da465e4c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 4 Jan 2025 19:56:56 +0100 Subject: [PATCH] Add AST source generator projects --- Directory.Packages.props | 2 + .../DecompilerAstNodeAttribute.cs | 13 + ...de.Decompiler.Generators.Attributes.csproj | 7 + .../DecompilerSyntaxTreeGenerator.cs | 289 ++++++++++++++++++ .../GeneratorAttributes.cs | 8 + .../ICSharpCode.Decompiler.Generators.csproj | 25 ++ .../IsExternalInit.cs | 3 + .../RoslynHelpers.cs | 32 ++ .../TreeTraversal.cs | 125 ++++++++ ICSharpCode.Decompiler/packages.lock.json | 3 + ILSpy.sln | 12 + 11 files changed, 519 insertions(+) create mode 100644 ICSharpCode.Decompiler.Generators.Attributes/DecompilerAstNodeAttribute.cs create mode 100644 ICSharpCode.Decompiler.Generators.Attributes/ICSharpCode.Decompiler.Generators.Attributes.csproj create mode 100644 ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs create mode 100644 ICSharpCode.Decompiler.Generators/GeneratorAttributes.cs create mode 100644 ICSharpCode.Decompiler.Generators/ICSharpCode.Decompiler.Generators.csproj create mode 100644 ICSharpCode.Decompiler.Generators/IsExternalInit.cs create mode 100644 ICSharpCode.Decompiler.Generators/RoslynHelpers.cs create mode 100644 ICSharpCode.Decompiler.Generators/TreeTraversal.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 51879bb08..55b538fc4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,8 +15,10 @@ + + diff --git a/ICSharpCode.Decompiler.Generators.Attributes/DecompilerAstNodeAttribute.cs b/ICSharpCode.Decompiler.Generators.Attributes/DecompilerAstNodeAttribute.cs new file mode 100644 index 000000000..b0d1924dc --- /dev/null +++ b/ICSharpCode.Decompiler.Generators.Attributes/DecompilerAstNodeAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace ICSharpCode.Decompiler.CSharp.Syntax +{ + public sealed class DecompilerAstNodeAttribute : Attribute + { + public DecompilerAstNodeAttribute(bool hasNullNode) { } + } + + public sealed class ExcludeFromMatchAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Generators.Attributes/ICSharpCode.Decompiler.Generators.Attributes.csproj b/ICSharpCode.Decompiler.Generators.Attributes/ICSharpCode.Decompiler.Generators.Attributes.csproj new file mode 100644 index 000000000..dbdcea46b --- /dev/null +++ b/ICSharpCode.Decompiler.Generators.Attributes/ICSharpCode.Decompiler.Generators.Attributes.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs b/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs new file mode 100644 index 000000000..c11027f53 --- /dev/null +++ b/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs @@ -0,0 +1,289 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Text; + +using ICSharpCode.Decompiler.CSharp.Syntax; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace ICSharpCode.Decompiler.Generators; + +[Generator] +internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator +{ + record AstNodeAdditions(string NodeName, bool NeedsAcceptImpls, bool NeedsVisitor, bool NeedsNullNode, int NullNodeBaseCtorParamCount, bool IsTypeNode, string VisitMethodName, string VisitMethodParamType, EquatableArray<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny)>? MembersToMatch); + + AstNodeAdditions GetAstNodeAdditions(GeneratorAttributeSyntaxContext context, CancellationToken ct) + { + var targetSymbol = (INamedTypeSymbol)context.TargetSymbol; + var attribute = context.Attributes.SingleOrDefault(ad => ad.AttributeClass?.Name == "DecompilerAstNodeAttribute")!; + var (visitMethodName, paramTypeName) = targetSymbol.Name switch { + "ErrorExpression" => ("ErrorNode", "AstNode"), + string s when s.Contains("AstType") => (s.Replace("AstType", "Type"), s), + _ => (targetSymbol.Name, targetSymbol.Name), + }; + + List<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny)>? membersToMatch = null; + + if (!targetSymbol.MemberNames.Contains("DoMatch")) + { + membersToMatch = new(); + + var astNodeType = (INamedTypeSymbol)context.SemanticModel.GetSpeculativeSymbolInfo(context.TargetNode.Span.Start, SyntaxFactory.ParseTypeName("AstNode"), SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol!; + + if (targetSymbol.BaseType!.MemberNames.Contains("MatchAttributesAndModifiers")) + membersToMatch.Add(("MatchAttributesAndModifiers", null!, false, false)); + + foreach (var m in targetSymbol.GetMembers()) + { + if (m is not IPropertySymbol property || property.IsIndexer || property.IsOverride) + continue; + if (property.GetAttributes().Any(a => a.AttributeClass?.Name == nameof(ExcludeFromMatchAttribute))) + continue; + if (property.Type.MetadataName is "CSharpTokenNode" or "TextLocation") + continue; + switch (property.Type) + { + case INamedTypeSymbol named when named.IsDerivedFrom(astNodeType) || named.MetadataName == "AstNodeCollection`1": + membersToMatch.Add((property.Name, named.Name, true, false)); + break; + case INamedTypeSymbol { TypeKind: TypeKind.Enum } named when named.GetMembers().Any(_ => _.Name == "Any"): + membersToMatch.Add((property.Name, named.Name, false, true)); + break; + default: + membersToMatch.Add((property.Name, property.Type.Name, false, false)); + break; + } + } + } + + return new(targetSymbol.Name, !targetSymbol.MemberNames.Contains("AcceptVisitor"), + NeedsVisitor: !targetSymbol.IsAbstract && targetSymbol.BaseType!.IsAbstract, + NeedsNullNode: (bool)attribute.ConstructorArguments[0].Value!, + NullNodeBaseCtorParamCount: targetSymbol.InstanceConstructors.Min(m => m.Parameters.Length), + IsTypeNode: targetSymbol.Name == "AstType" || targetSymbol.BaseType?.Name == "AstType", + visitMethodName, paramTypeName, membersToMatch?.ToEquatableArray()); + } + + void WriteGeneratedMembers(SourceProductionContext context, AstNodeAdditions source) + { + var builder = new StringBuilder(); + + builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); + builder.AppendLine(); + + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + + builder.AppendLine($"partial class {source.NodeName}"); + builder.AppendLine("{"); + + if (source.NeedsNullNode) + { + bool needsNew = source.NodeName != "AstNode"; + + builder.AppendLine($" {(needsNew ? "new " : "")}public static readonly {source.NodeName} Null = new Null{source.NodeName}();"); + + builder.AppendLine($@" + sealed class Null{source.NodeName} : {source.NodeName} + {{ + public override NodeType NodeType => NodeType.Unknown; + + public override bool IsNull => true; + + public override void AcceptVisitor(IAstVisitor visitor) + {{ + visitor.VisitNullNode(this); + }} + + public override T AcceptVisitor(IAstVisitor visitor) + {{ + return visitor.VisitNullNode(this); + }} + + public override S AcceptVisitor(IAstVisitor visitor, T data) + {{ + return visitor.VisitNullNode(this, data); + }} + + protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match) + {{ + return other == null || other.IsNull; + }}"); + + if (source.IsTypeNode) + { + builder.AppendLine( + $@" + + public override Decompiler.TypeSystem.ITypeReference ToTypeReference(Resolver.NameLookupMode lookupMode, Decompiler.TypeSystem.InterningProvider? interningProvider = null) + {{ + return Decompiler.TypeSystem.SpecialType.UnknownType; + }}" + ); + } + + if (source.NullNodeBaseCtorParamCount > 0) + { + builder.AppendLine($@" + + public Null{source.NodeName}() : base({string.Join(", ", Enumerable.Repeat("default", source.NullNodeBaseCtorParamCount))}) {{ }}"); + } + + builder.AppendLine($@" + }} + +"); + + } + + if (source.NeedsAcceptImpls && source.NeedsVisitor) + { + builder.Append($@" public override void AcceptVisitor(IAstVisitor visitor) + {{ + visitor.Visit{source.NodeName}(this); + }} + + public override T AcceptVisitor(IAstVisitor visitor) + {{ + return visitor.Visit{source.NodeName}(this); + }} + + public override S AcceptVisitor(IAstVisitor visitor, T data) + {{ + return visitor.Visit{source.NodeName}(this, data); + }} +"); + } + + if (source.MembersToMatch != null) + { + builder.Append($@" protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match) + {{ + return other is {source.NodeName} o && !o.IsNull"); + + foreach (var (member, typeName, recursive, hasAny) in source.MembersToMatch) + { + if (member == "MatchAttributesAndModifiers") + { + builder.Append($"\r\n\t\t\t&& this.MatchAttributesAndModifiers(o, match)"); + } + else if (recursive) + { + builder.Append($"\r\n\t\t\t&& this.{member}.DoMatch(o.{member}, match)"); + } + else if (hasAny) + { + builder.Append($"\r\n\t\t\t&& (this.{member} == {typeName}.Any || this.{member} == o.{member})"); + } + else + { + builder.Append($"\r\n\t\t\t&& this.{member} == o.{member}"); + } + } + + builder.Append(@"; + } +"); + } + + builder.AppendLine("}"); + + context.AddSource(source.NodeName + ".g.cs", SourceText.From(builder.ToString(), Encoding.UTF8)); + } + + private void WriteVisitors(SourceProductionContext context, ImmutableArray source) + { + var builder = new StringBuilder(); + + builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); + + source = source + .Concat([new("NullNode", false, true, false, 0, false, "NullNode", "AstNode", null), new("PatternPlaceholder", false, true, false, 0, false, "PatternPlaceholder", "AstNode", null)]) + .ToImmutableArray(); + + WriteInterface("IAstVisitor", "void", ""); + WriteInterface("IAstVisitor", "S", ""); + WriteInterface("IAstVisitor", "S", ", T data"); + + context.AddSource("IAstVisitor.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8)); + + void WriteInterface(string name, string ret, string param) + { + builder.AppendLine($"public interface {name}"); + builder.AppendLine("{"); + + foreach (var type in source.OrderBy(t => t.VisitMethodName)) + { + if (!type.NeedsVisitor) + continue; + + string extParams, paramName; + if (type.VisitMethodName == "PatternPlaceholder") + { + paramName = "placeholder"; + extParams = ", PatternMatching.Pattern pattern" + param; + } + else + { + paramName = char.ToLowerInvariant(type.VisitMethodName[0]) + type.VisitMethodName.Substring(1); + extParams = param; + } + + builder.AppendLine($"\t{ret} Visit{type.VisitMethodName}({type.VisitMethodParamType} {paramName}{extParams});"); + } + + builder.AppendLine("}"); + } + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var astNodeAdditions = context.SyntaxProvider.ForAttributeWithMetadataName( + "ICSharpCode.Decompiler.CSharp.Syntax.DecompilerAstNodeAttribute", + (n, ct) => n is ClassDeclarationSyntax, + GetAstNodeAdditions); + + var visitorMembers = astNodeAdditions.Collect(); + + context.RegisterSourceOutput(astNodeAdditions, WriteGeneratedMembers); + context.RegisterSourceOutput(visitorMembers, WriteVisitors); + } +} + +readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + readonly T[] array; + + public EquatableArray(T[] array) + { + this.array = array ?? throw new ArgumentNullException(nameof(array)); + } + + public bool Equals(EquatableArray other) + { + return other.array.AsSpan().SequenceEqual(this.array); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)array).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return array.GetEnumerator(); + } +} + +static class EquatableArrayExtensions +{ + public static EquatableArray ToEquatableArray(this List array) where T : IEquatable + { + return new EquatableArray(array.ToArray()); + } +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Generators/GeneratorAttributes.cs b/ICSharpCode.Decompiler.Generators/GeneratorAttributes.cs new file mode 100644 index 000000000..b730ca53c --- /dev/null +++ b/ICSharpCode.Decompiler.Generators/GeneratorAttributes.cs @@ -0,0 +1,8 @@ +using System; + +namespace ICSharpCode.Decompiler.CSharp.Syntax; + +public class DecompilerAstNodeAttribute(bool hasNullNode) : Attribute +{ + public bool HasNullNode { get; } = hasNullNode; +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Generators/ICSharpCode.Decompiler.Generators.csproj b/ICSharpCode.Decompiler.Generators/ICSharpCode.Decompiler.Generators.csproj new file mode 100644 index 000000000..6bb2025e4 --- /dev/null +++ b/ICSharpCode.Decompiler.Generators/ICSharpCode.Decompiler.Generators.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + enable + enable + true + 12 + + + + 4.8.0 + + + + + + + + + + + + + diff --git a/ICSharpCode.Decompiler.Generators/IsExternalInit.cs b/ICSharpCode.Decompiler.Generators/IsExternalInit.cs new file mode 100644 index 000000000..7bac4c969 --- /dev/null +++ b/ICSharpCode.Decompiler.Generators/IsExternalInit.cs @@ -0,0 +1,3 @@ +namespace System.Runtime.CompilerServices; + +class IsExternalInit { } diff --git a/ICSharpCode.Decompiler.Generators/RoslynHelpers.cs b/ICSharpCode.Decompiler.Generators/RoslynHelpers.cs new file mode 100644 index 000000000..fa4747f76 --- /dev/null +++ b/ICSharpCode.Decompiler.Generators/RoslynHelpers.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; + +namespace ICSharpCode.Decompiler.Generators; + +public static class RoslynHelpers +{ + public static IEnumerable GetTopLevelTypes(this IAssemblySymbol assembly) + { + foreach (var ns in TreeTraversal.PreOrder(assembly.GlobalNamespace, ns => ns.GetNamespaceMembers())) + { + foreach (var t in ns.GetTypeMembers()) + { + yield return t; + } + } + } + + public static bool IsDerivedFrom(this INamedTypeSymbol type, INamedTypeSymbol baseType) + { + INamedTypeSymbol? t = type; + + while (t != null) + { + if (SymbolEqualityComparer.Default.Equals(t, baseType)) + return true; + + t = t.BaseType; + } + + return false; + } +} diff --git a/ICSharpCode.Decompiler.Generators/TreeTraversal.cs b/ICSharpCode.Decompiler.Generators/TreeTraversal.cs new file mode 100644 index 000000000..4c194383a --- /dev/null +++ b/ICSharpCode.Decompiler.Generators/TreeTraversal.cs @@ -0,0 +1,125 @@ +#nullable enable +// Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace ICSharpCode.Decompiler.Generators; + +/// +/// Static helper methods for traversing trees. +/// +internal static class TreeTraversal +{ + /// + /// Converts a tree data structure into a flat list by traversing it in pre-order. + /// + /// The root element of the tree. + /// The function that gets the children of an element. + /// Iterator that enumerates the tree structure in pre-order. + public static IEnumerable PreOrder(T root, Func?> recursion) + { + return PreOrder(new T[] { root }, recursion); + } + + /// + /// Converts a tree data structure into a flat list by traversing it in pre-order. + /// + /// The root elements of the forest. + /// The function that gets the children of an element. + /// Iterator that enumerates the tree structure in pre-order. + public static IEnumerable PreOrder(IEnumerable input, Func?> recursion) + { + Stack> stack = new Stack>(); + try + { + stack.Push(input.GetEnumerator()); + while (stack.Count > 0) + { + while (stack.Peek().MoveNext()) + { + T element = stack.Peek().Current; + yield return element; + IEnumerable? children = recursion(element); + if (children != null) + { + stack.Push(children.GetEnumerator()); + } + } + stack.Pop().Dispose(); + } + } + finally + { + while (stack.Count > 0) + { + stack.Pop().Dispose(); + } + } + } + + /// + /// Converts a tree data structure into a flat list by traversing it in post-order. + /// + /// The root element of the tree. + /// The function that gets the children of an element. + /// Iterator that enumerates the tree structure in post-order. + public static IEnumerable PostOrder(T root, Func?> recursion) + { + return PostOrder(new T[] { root }, recursion); + } + + /// + /// Converts a tree data structure into a flat list by traversing it in post-order. + /// + /// The root elements of the forest. + /// The function that gets the children of an element. + /// Iterator that enumerates the tree structure in post-order. + public static IEnumerable PostOrder(IEnumerable input, Func?> recursion) + { + Stack> stack = new Stack>(); + try + { + stack.Push(input.GetEnumerator()); + while (stack.Count > 0) + { + while (stack.Peek().MoveNext()) + { + T element = stack.Peek().Current; + IEnumerable? children = recursion(element); + if (children != null) + { + stack.Push(children.GetEnumerator()); + } + else + { + yield return element; + } + } + stack.Pop().Dispose(); + if (stack.Count > 0) + yield return stack.Peek().Current; + } + } + finally + { + while (stack.Count > 0) + { + stack.Pop().Dispose(); + } + } + } +} diff --git a/ICSharpCode.Decompiler/packages.lock.json b/ICSharpCode.Decompiler/packages.lock.json index e44c80edb..316fa0f4e 100644 --- a/ICSharpCode.Decompiler/packages.lock.json +++ b/ICSharpCode.Decompiler/packages.lock.json @@ -91,6 +91,9 @@ "type": "Transitive", "resolved": "6.0.0", "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "icsharpcode.decompiler.generators.attributes": { + "type": "Project" } } } diff --git a/ILSpy.sln b/ILSpy.sln index 099c49504..f6a5888a1 100644 --- a/ILSpy.sln +++ b/ILSpy.sln @@ -43,6 +43,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ICSharpCode.Decompiler.Generators", "ICSharpCode.Decompiler.Generators\ICSharpCode.Decompiler.Generators.csproj", "{0792B524-622B-4B9D-8027-3531ED6A42EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ICSharpCode.Decompiler.Generators.Attributes", "ICSharpCode.Decompiler.Generators.Attributes\ICSharpCode.Decompiler.Generators.Attributes.csproj", "{02FE14EC-EEA7-4902-BCBF-0F95FB90C9B3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +105,14 @@ Global {81A30182-3378-4952-8880-F44822390040}.Debug|Any CPU.Build.0 = Debug|Any CPU {81A30182-3378-4952-8880-F44822390040}.Release|Any CPU.ActiveCfg = Release|Any CPU {81A30182-3378-4952-8880-F44822390040}.Release|Any CPU.Build.0 = Release|Any CPU + {0792B524-622B-4B9D-8027-3531ED6A42EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0792B524-622B-4B9D-8027-3531ED6A42EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0792B524-622B-4B9D-8027-3531ED6A42EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0792B524-622B-4B9D-8027-3531ED6A42EB}.Release|Any CPU.Build.0 = Release|Any CPU + {02FE14EC-EEA7-4902-BCBF-0F95FB90C9B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02FE14EC-EEA7-4902-BCBF-0F95FB90C9B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02FE14EC-EEA7-4902-BCBF-0F95FB90C9B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02FE14EC-EEA7-4902-BCBF-0F95FB90C9B3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE