diff --git a/Tsu.TreeSourceGen/src/AnalyzerReleases.Shipped.md b/Tsu.TreeSourceGen/src/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..60c1edf --- /dev/null +++ b/Tsu.TreeSourceGen/src/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/Tsu.TreeSourceGen/src/AnalyzerReleases.Unshipped.md b/Tsu.TreeSourceGen/src/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..4a284dc --- /dev/null +++ b/Tsu.TreeSourceGen/src/AnalyzerReleases.Unshipped.md @@ -0,0 +1,9 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +TSG0001 | Tsu.TreeSourceGen | Error | TsgDiagnostics +TSG0002 | Tsu.TreeSourceGen | Error | TsgDiagnostics \ No newline at end of file diff --git a/Tsu.TreeSourceGen/src/CodeConstants.cs b/Tsu.TreeSourceGen/src/CodeConstants.cs new file mode 100644 index 0000000..07a8e7e --- /dev/null +++ b/Tsu.TreeSourceGen/src/CodeConstants.cs @@ -0,0 +1,76 @@ +// Copyright © 2024 GGG KILLER +// +// 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 Tsu.TreeSourceGen; + +internal static class CodeConstants +{ + public const string Namespace = "Tsu.TreeSourceGen"; + + internal static class TreeNodeAttribute + { + public const string FullName = "TreeNodeAttribute"; + + public const string SourceCode = $$""" + using System; + + namespace {{Namespace}}; + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class {{FullName}}(Type treeRoot) : Attribute + { + public Type TreeRoot { get; } = treeRoot; + + public string? Name { get; set; } + } + """; + } + + internal static class TreeVisitorAttribute + { + public const string FullName = "TreeVisitorAttribute"; + + public const string SourceCode = $$""" + using System; + + namespace {{Namespace}}; + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class {{FullName}}(Type treeRoot) : Attribute + { + public Type TreeRoot { get; } = treeRoot; + } + """; + } + + internal static class TreeWalkerAttribute + { + public const string FullName = "TreeWalkerAttribute"; + + public const string SourceCode = $$""" + using System; + + namespace {{Namespace}}; + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class {{FullName}}(Type treeRoot) : Attribute + { + public Type TreeRoot { get; } = treeRoot; + } + """; + } +} \ No newline at end of file diff --git a/Tsu.TreeSourceGen/src/Generator.cs b/Tsu.TreeSourceGen/src/Generator.cs new file mode 100644 index 0000000..1eecf3b --- /dev/null +++ b/Tsu.TreeSourceGen/src/Generator.cs @@ -0,0 +1,323 @@ +// Copyright © 2024 GGG KILLER +// +// 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. + +using System.CodeDom.Compiler; +using System.Globalization; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Tsu.TreeSourceGen; + +/// +/// The main source generator of this library. +/// +[Generator(LanguageNames.CSharp)] +public sealed class Generator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => + { + ctx.AddSource($"{CodeConstants.TreeNodeAttribute.FullName}.g.cs", CodeConstants.TreeNodeAttribute.SourceCode); + ctx.AddSource($"{CodeConstants.TreeVisitorAttribute.FullName}.g.cs", CodeConstants.TreeVisitorAttribute.SourceCode); + ctx.AddSource($"{CodeConstants.TreeWalkerAttribute.FullName}.g.cs", CodeConstants.TreeWalkerAttribute.SourceCode); + }); + + // This pipeline finds all marked nodes and then proceeds to assemble the tree "structure" from them. + // (structure in quote because we don't look at inheritance as it's not necessary) + var trees = context.GetTrees(); + + // Map all visitors that were defined + var visitors = context.GetVisitors($"{CodeConstants.Namespace}.{CodeConstants.TreeVisitorAttribute.FullName}").Collect(); + + // Collect all declared walkers + var walkers = context.GetVisitors($"{CodeConstants.Namespace}.{CodeConstants.TreeWalkerAttribute.FullName}"); + + // Make the final pairs + var visitorPairs = trees.MakeVisitorPairs(visitors); + var walkerSets = trees.MakeWalkerSets(visitors, walkers); + + // Validate that the nodes properly inherit from the root. + context.RegisterSourceOutput(trees, (ctx, tree) => + { + foreach (var node in tree.Nodes) + { + var inherits = false; + for (var parent = node.TypeSymbol; parent is not null; parent = parent.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(tree.Root, parent)) + inherits = true; + } + + if (!inherits) + { + ctx.ReportDiagnostic(TsgDiagnostics.Create( + descriptor: TsgDiagnostics.NodeDoesNotInheritFromRoot, + node.TypeSymbol, + messageArgs: node.TypeSymbol.Name)); + } + } + }); + + // Write out all the code + var namespaces = new[] + { + "System", + "System.Collections.Generic", + "System.Diagnostics.CodeAnalysis", + }; + + context.RegisterSourceOutput( + visitorPairs, + (ctx, pair) => + { + var tree = pair.Tree; + var visitors = pair.Visitors; + + var rootNs = tree.Root.GetContainingNamespace(); + + var builder = new StringBuilder(); + var writer = new VisitorWriter(builder); + + foreach (var visitor in visitors!.Visitors) + { + builder.Clear(); + + writer.WriteFileHeader(); + + foreach (var ns in namespaces) + { + writer.Write("using "); + writer.Write(ns); + writer.WriteLine(';'); + } + + foreach (var node in tree.Nodes) + { + writer.WriteLine(); + writer.Write("namespace "); + writer.WriteLine(node.TypeSymbol.GetContainingNamespace()); + writer.WriteLine('{'); + writer.Indent++; + + writer.Indent--; + writer.WriteLine('}'); + } + + writer.WriteLine(); + writer.Write("namespace "); + writer.WriteLine(visitor.Namespace); + writer.WriteLine('{'); + writer.Indent++; + writer.WithParents(visitor.RootClass, vclass => + { + // Write out the interface + { + writer.Write("interface I"); + writer.Write(vclass.Name); + writer.WriteTypeParameterList(visitor.Arity); + writer.WriteLine(); + writer.WriteLine('{'); + writer.Indent++; + + foreach (var node in tree.Nodes) + { + writer.WriteSignature(node, visitor); + writer.WriteLine(';'); + } + + writer.Indent--; + writer.WriteLine('}'); + } + + // Write out the base class + { + writer.Write("partial "); + writer.Write(vclass.Keyword); + writer.Write(' '); + writer.Write(vclass.Name); + writer.WriteTypeParameterList(visitor.Arity); + writer.Write(" : I"); + writer.Write(vclass.Name); + writer.WriteTypeParameterList(visitor.Arity); + writer.Write(' '); + writer.WriteLine(vclass.Constraints); + writer.WriteLine('{'); + writer.Indent++; + { + // Entry visit method + { + writer.Write("public virtual "); + var arg = writer.WriteSignature(rootNs, tree.Root.Name, tree.Root.Name, visitor.Arity); + writer.Write('{'); + writer.Indent++; + + writer.Write("if ("); + writer.Write(arg); + writer.WriteLine(" is not null)"); + writer.Indent++; + writer.Write("return "); + writer.Write(arg); + writer.Write(".Accept("); + writer.Write("this"); + for (var idx = 1; idx < visitor.Arity; idx++) + { + writer.Write(", "); + writer.Write("arg"); + writer.Write(idx); + } + writer.WriteLine(");"); + + writer.WriteLine(); + writer.WriteLine("return default;"); + writer.Indent--; + writer.Write('}'); + } + + writer.WriteLine(); + + // Default Visit Method + { + writer.Write("public virtual "); + writer.Write(visitor.Arity > 0 ? "TReturn " : "void "); + writer.Write("DefaultVisit"); + writer.Write('('); + + writer.Write(rootNs); + writer.Write('.'); + writer.Write(tree.Root.Name); + writer.Write(' '); + writer.Write("node"); + for (var idx = 1; idx < visitor.Arity; idx++) + { + writer.Write(", "); + writer.Write("TArg"); + writer.Write(idx); + writer.Write(" arg"); + writer.Write(idx); + } + writer.WriteLine(") => default;"); + } + + foreach (var node in tree.Nodes) + { + writer.WriteLine(); + var arg = writer.WriteSignature(node, visitor); + writer.WriteLine(" => DefaultVisit("); + writer.Write(arg); + for (var idx = 1; idx < visitor.Arity; idx++) + { + writer.Write(", arg"); + writer.Write(idx); + } + writer.WriteLine(");"); + } + } + writer.Indent--; + writer.WriteLine('}'); + } + }); + writer.Indent--; + writer.WriteLine('}'); + + ctx.AddSource($"{tree.Root.Name}.{visitor.RootClass.Last().Name}`{visitor.Arity}", builder.ToString()); + } + }); + } +} + +internal sealed class VisitorWriter(StringBuilder sb) : IndentedTextWriter(new StringWriter(sb, CultureInfo.InvariantCulture)) +{ + public void WriteFileHeader() + { + WriteLine("// "); + WriteLine(); + WriteLine("#nullable enable"); + WriteLine(); + } + + public void WithParents(ParentClass root, Action action) + { + var count = Indent; + ParentClass? c; + for (c = root; c?.Child is not null; c = c.Child) + { + Write("partial "); + Write(c.Keyword); + Write(' '); + Write(c.Name); + Write(' '); + WriteLine(c.Constraints); + WriteLine("{"); + + Indent++; + } + + action(c!); + + while (Indent > count) + { + Indent--; + WriteLine("}"); + } + } + + public void WriteTypeParameterList(int count) + { + if (count > 0) + { + Write('<'); + Write("TReturn"); + + for (var idx = 1; idx < count; idx++) + { + Write(", TArg"); + Write(idx); + } + Write('>'); + } + } + + public string WriteSignature(Node node, Visitor visitor) => + WriteSignature(node.Namespace, node.TypeSymbol.Name, node.Name ?? node.TypeSymbol.Name, visitor.Arity); + + public string WriteSignature(string typeNamespace, string typeName, string nodeName, int arity) + { + Write(arity > 0 ? "TReturn " : "void "); + Write("Visit"); + Write(nodeName); + Write('('); + + Write(typeNamespace); + Write('.'); + Write(typeName); + Write(' '); + var nodeArgName = char.ToLower(nodeName[0]) + nodeName.Substring(1); + Write(nodeArgName); + for (var idx = 1; idx < arity; idx++) + { + Write(", "); + Write("TArg"); + Write(idx); + Write(" arg"); + Write(idx); + } + WriteLine(')'); + return nodeArgName; + } +} \ No newline at end of file diff --git a/Tsu.TreeSourceGen/src/GeneratorExtensions.cs b/Tsu.TreeSourceGen/src/GeneratorExtensions.cs new file mode 100644 index 0000000..baa13ec --- /dev/null +++ b/Tsu.TreeSourceGen/src/GeneratorExtensions.cs @@ -0,0 +1,123 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Tsu.TreeSourceGen; + +internal static class GeneratorExtensions +{ + public static IncrementalValuesProvider GetTrees(this IncrementalGeneratorInitializationContext context) + { + return context.SyntaxProvider.ForAttributeWithMetadataName( + $"{CodeConstants.Namespace}.{CodeConstants.TreeNodeAttribute.FullName}", + (node, _) => node.IsKind(SyntaxKind.ClassDeclaration), + (ctx, _) => + { + var attr = ctx.Attributes.Single(); + if (attr.ConstructorArguments.Single().Value is not INamedTypeSymbol targetType || targetType.TypeKind != TypeKind.Class) + { + return (null!, null!, null!, null); + } + + string? name = null; + if (attr.NamedArguments.SingleOrDefault(x => x.Key == "Name").Value.Value is string n + && !string.IsNullOrWhiteSpace(n)) + { + name = n; + } + + return ( + Root: targetType, + ParentClass: ((ClassDeclarationSyntax) ctx.TargetNode).GetContainingTypes(), + NodeSymbol: (INamedTypeSymbol) ctx.TargetSymbol, + Name: name + ); + }) + .Where(x => x.NodeSymbol is not null && x.Root is not null) + .Collect() + .SelectMany((nodes, cancellationToken) => + { + var groups = nodes.GroupBy(node => node.Root, SymbolEqualityComparer.Default); + var builder = ImmutableArray.CreateBuilder(groups.Count()); + foreach (var group in groups) + { + cancellationToken.ThrowIfCancellationRequested(); + + var treeNodes = group.Select(node => new Node(node.ParentClass, node.NodeSymbol, node.Name)); + + builder.Add(new Tree((INamedTypeSymbol) group.Key!, treeNodes)); + } + return builder.MoveToImmutable(); + }); + } + + public static IncrementalValuesProvider GetVisitors(this IncrementalGeneratorInitializationContext context, string fullyQualifiedMetadataName) + { + return context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + (node, _) => node.IsKind(SyntaxKind.ClassDeclaration), + (ctx, _) => + { + var attr = ctx.Attributes.Single(); + if (attr.ConstructorArguments.Single().Value is not INamedTypeSymbol targetType || targetType.TypeKind != TypeKind.Class) + { + return (null!, null!); + } + + return ( + Root: targetType, + Visitor: new Visitor( + ctx.TargetSymbol.GetContainingNamespace(), + ((ClassDeclarationSyntax) ctx.TargetNode).GetContainingTypes(), + ((INamedTypeSymbol) ctx.TargetSymbol).Arity) + ); + }) + .Where(x => x.Root is not null) + .Collect() + .SelectMany((visitors, cancellationToken) => + { + var groups = visitors.GroupBy(visitor => visitor.Root, SymbolEqualityComparer.Default); + + var builder = ImmutableArray.CreateBuilder(groups.Count()); + foreach (var group in groups) + { + cancellationToken.ThrowIfCancellationRequested(); + + builder.Add(new VisitorSet( + (INamedTypeSymbol) group.Key!, + group.Select(v => v.Visitor).ToImmutableArray())); + } + return builder.MoveToImmutable(); + }); + } + + public static IncrementalValuesProvider<(Tree Tree, VisitorSet Visitors)> MakeVisitorPairs(this IncrementalValuesProvider trees, IncrementalValueProvider> visitors) + { + return trees.Combine(visitors) + .Select((pair, cancellationToken) => + { + var tree = pair.Left; + var visitorSet = pair.Right.SingleOrDefault(s => SymbolEqualityComparer.Default.Equals(tree.Root, s.Root)); + + return (Tree: tree, Visitors: visitorSet); + }) + .Where(x => x.Visitors is not null)!; + } + + public static IncrementalValuesProvider<(Tree Tree, VisitorSet Visitors, VisitorSet Walkers)> MakeWalkerSets(this IncrementalValuesProvider trees, IncrementalValueProvider> visitors, IncrementalValuesProvider walkers) + { + return trees.Combine(visitors) + .Combine(walkers.Collect()) + .Select((x, _) => (Tree: x.Left.Left, Visitors: x.Left.Right, Walkers: x.Right)) + .Select((pair, cancellationToken) => + { + var tree = pair.Tree; + var visitorSet = pair.Visitors.SingleOrDefault(s => SymbolEqualityComparer.Default.Equals(tree.Root, s.Root)); + var walkerSet = pair.Walkers.SingleOrDefault(s => SymbolEqualityComparer.Default.Equals(tree.Root, s.Root)); + + return (Tree: tree, Visitor: visitorSet, Walkers: walkerSet); + }) + .Where(x => x.Walkers is not null)!; + } +} \ No newline at end of file diff --git a/Tsu.TreeSourceGen/src/Model/Node.cs b/Tsu.TreeSourceGen/src/Model/Node.cs new file mode 100644 index 0000000..8943d8d --- /dev/null +++ b/Tsu.TreeSourceGen/src/Model/Node.cs @@ -0,0 +1,36 @@ +// Copyright © 2024 GGG KILLER +// +// 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. + +using Microsoft.CodeAnalysis; + +namespace Tsu.TreeSourceGen; + +internal sealed class Node(ParentClass parentClass, INamedTypeSymbol typeSymbol, string? name) : IEquatable +{ + public ParentClass ParentClass { get; } = parentClass; + public INamedTypeSymbol TypeSymbol { get; } = typeSymbol; + public string? Name { get; } = name; + public string Namespace { get; } = typeSymbol.GetContainingNamespace(); + + public override bool Equals(object obj) => Equals(obj as Node); + public bool Equals(Node? other) => + SymbolEqualityComparer.Default.Equals(TypeSymbol, other?.TypeSymbol) + && string.Equals(Name, other.Name); + + public override int GetHashCode() => + HashCode.Combine(SymbolEqualityComparer.Default.GetHashCode(TypeSymbol), Name); +} diff --git a/Tsu.TreeSourceGen/src/Model/Tree.cs b/Tsu.TreeSourceGen/src/Model/Tree.cs new file mode 100644 index 0000000..0da90f7 --- /dev/null +++ b/Tsu.TreeSourceGen/src/Model/Tree.cs @@ -0,0 +1,38 @@ +// Copyright © 2024 GGG KILLER +// +// 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. + +using Microsoft.CodeAnalysis; + +namespace Tsu.TreeSourceGen; + +internal sealed class Tree(INamedTypeSymbol root, IEnumerable nodes) : IEquatable +{ + public INamedTypeSymbol Root { get; } = root; + public IEnumerable Nodes { get; } = nodes; + + public override bool Equals(object obj) => Equals(obj as Tree); + public bool Equals(Tree? other) => SymbolEqualityComparer.Default.Equals(Root, other?.Root) && Nodes.SequenceEqual(other.Nodes); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(Root, SymbolEqualityComparer.Default); + foreach (var node in Nodes) + hash.Add(node); + return hash.ToHashCode(); + } +} diff --git a/Tsu.TreeSourceGen/src/Model/Visitor.cs b/Tsu.TreeSourceGen/src/Model/Visitor.cs new file mode 100644 index 0000000..37278ce --- /dev/null +++ b/Tsu.TreeSourceGen/src/Model/Visitor.cs @@ -0,0 +1,19 @@ +// Copyright © 2024 GGG KILLER +// +// 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 Tsu.TreeSourceGen; + +internal sealed record Visitor(string Namespace, ParentClass RootClass, int Arity); diff --git a/Tsu.TreeSourceGen/src/Model/VisitorSet.cs b/Tsu.TreeSourceGen/src/Model/VisitorSet.cs new file mode 100644 index 0000000..8be470c --- /dev/null +++ b/Tsu.TreeSourceGen/src/Model/VisitorSet.cs @@ -0,0 +1,40 @@ +// Copyright © 2024 GGG KILLER +// +// 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. + +using Microsoft.CodeAnalysis; + +namespace Tsu.TreeSourceGen; + +internal sealed class VisitorSet(INamedTypeSymbol root, IEnumerable visitors) : IEquatable +{ + public INamedTypeSymbol Root { get; } = root; + public IEnumerable Visitors { get; } = visitors; + + public override bool Equals(object obj) => Equals(obj as VisitorSet); + public bool Equals(VisitorSet? other) => + SymbolEqualityComparer.Default.Equals(Root, other?.Root) + && Visitors.SequenceEqual(other.Visitors); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(Root, SymbolEqualityComparer.Default); + foreach (var visitor in Visitors) + hash.Add(visitor); + return hash.ToHashCode(); + } +} diff --git a/Tsu.TreeSourceGen/src/ParentClass.cs b/Tsu.TreeSourceGen/src/ParentClass.cs new file mode 100644 index 0000000..333a1cb --- /dev/null +++ b/Tsu.TreeSourceGen/src/ParentClass.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// Sourced from https://github.com/dotnet/Open-XML-SDK/blob/35b91d3c18fc06379b74f0980e3498f2b04c1fc8/gen/DocumentFormat.OpenXml.Generator/ParentClass.cs + +namespace Tsu.TreeSourceGen; + +internal sealed record class ParentClass(string Keyword, string Name, string Constraints, ParentClass? Child) : IEnumerable +{ + public IEnumerator GetEnumerator() + { + ParentClass? child = this; + + while (child is not null) + { + yield return child; + child = child.Child; + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Tsu.TreeSourceGen/src/RoslynExtensions.cs b/Tsu.TreeSourceGen/src/RoslynExtensions.cs new file mode 100644 index 0000000..8ef6c78 --- /dev/null +++ b/Tsu.TreeSourceGen/src/RoslynExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// Sourced from https://github.com/dotnet/Open-XML-SDK/blob/35b91d3c18fc06379b74f0980e3498f2b04c1fc8/gen/DocumentFormat.OpenXml.Generator/RoslynExtensions.cs + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Tsu.TreeSourceGen; + +internal static class RoslynExtensions +{ + public static string GetContainingNamespace(this ISymbol? symbol) + { + var ns = symbol?.ContainingNamespace; + var stack = new Stack(); + + while (ns is not null) + { + if (!string.IsNullOrEmpty(ns.Name)) + { + stack.Push(ns.Name); + } + + ns = ns.ContainingNamespace; + } + + return string.Join(".", stack); + } + + public static ParentClass GetContainingTypes(this ClassDeclarationSyntax typeSyntax) + { + // Try and get the parent syntax. If it isn't a type like class/struct, this will be null + TypeDeclarationSyntax? parentSyntax = typeSyntax.Parent as TypeDeclarationSyntax; + var parentClassInfo = new ParentClass( + typeSyntax.Keyword.ValueText, + typeSyntax.Identifier.ToString() + typeSyntax.TypeParameterList, + typeSyntax.ConstraintClauses.ToString(), + null); + + // Keep looping while we're in a supported nested type + while (parentSyntax != null && IsAllowedKind(parentSyntax.Kind())) + { + // Record the parent type keyword (class/struct etc), name, and constraints + parentClassInfo = new( + Keyword: parentSyntax.Keyword.ValueText, + Name: parentSyntax.Identifier.ToString() + parentSyntax.TypeParameterList, + Constraints: parentSyntax.ConstraintClauses.ToString(), + Child: parentClassInfo); // set the child link (null initially) + + // Move to the next outer type + parentSyntax = parentSyntax.Parent as TypeDeclarationSyntax; + } + + // return a link to the outermost parent type + return parentClassInfo; + } + + // We can only be nested in class/struct/record + private static bool IsAllowedKind(SyntaxKind kind) => + kind is SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.RecordDeclaration; +} diff --git a/Tsu.TreeSourceGen/src/Shims/HashCode.cs b/Tsu.TreeSourceGen/src/Shims/HashCode.cs new file mode 100644 index 0000000..406189c --- /dev/null +++ b/Tsu.TreeSourceGen/src/Shims/HashCode.cs @@ -0,0 +1,443 @@ +// Copyright © 2024 GGG KILLER +// +// 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. + +/* + +The xxHash32 implementation is based on the code published by Yann Collet: +https://raw.githubusercontent.com/Cyan4973/xxHash/5c174cfa4e45a42f94082dc0d4539b39696afea1/xxhash.c + + xxHash - Fast Hash algorithm + Copyright (C) 2012-2016, Yann Collet + + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + You can contact the author at : + - xxHash homepage: http://www.xxhash.com + - xxHash source repository : https://github.com/Cyan4973/xxHash + +*/ + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Tsu.TreeSourceGen +{ + // xxHash32 is used for the hash code. + // https://github.com/Cyan4973/xxHash + + internal struct HashCode + { + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private uint _v1, _v2, _v3, _v4; + private uint _queue1, _queue2, _queue3; + private uint _length; + + private static uint GenerateGlobalSeed() + { + Unsafe.SkipInit(out uint result); + return result; + } + + public static int Combine(T1 value1) + { + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. + + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 4; + + hash = QueueRound(hash, hc1); + + hash = MixFinal(hash); + return (int) hash; + } + + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 8; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + + hash = MixFinal(hash); + return (int) hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + uint hc3 = (uint) (value3?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 12; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + hash = QueueRound(hash, hc3); + + hash = MixFinal(hash); + return (int) hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + uint hc3 = (uint) (value3?.GetHashCode() ?? 0); + uint hc4 = (uint) (value4?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 16; + + hash = MixFinal(hash); + return (int) hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + uint hc3 = (uint) (value3?.GetHashCode() ?? 0); + uint hc4 = (uint) (value4?.GetHashCode() ?? 0); + uint hc5 = (uint) (value5?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 20; + + hash = QueueRound(hash, hc5); + + hash = MixFinal(hash); + return (int) hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + uint hc3 = (uint) (value3?.GetHashCode() ?? 0); + uint hc4 = (uint) (value4?.GetHashCode() ?? 0); + uint hc5 = (uint) (value5?.GetHashCode() ?? 0); + uint hc6 = (uint) (value6?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 24; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + + hash = MixFinal(hash); + return (int) hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + uint hc3 = (uint) (value3?.GetHashCode() ?? 0); + uint hc4 = (uint) (value4?.GetHashCode() ?? 0); + uint hc5 = (uint) (value5?.GetHashCode() ?? 0); + uint hc6 = (uint) (value6?.GetHashCode() ?? 0); + uint hc7 = (uint) (value7?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 28; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + hash = QueueRound(hash, hc7); + + hash = MixFinal(hash); + return (int) hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + uint hc3 = (uint) (value3?.GetHashCode() ?? 0); + uint hc4 = (uint) (value4?.GetHashCode() ?? 0); + uint hc5 = (uint) (value5?.GetHashCode() ?? 0); + uint hc6 = (uint) (value6?.GetHashCode() ?? 0); + uint hc7 = (uint) (value7?.GetHashCode() ?? 0); + uint hc8 = (uint) (value8?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + v1 = Round(v1, hc5); + v2 = Round(v2, hc6); + v3 = Round(v3, hc7); + v4 = Round(v4, hc8); + + uint hash = MixState(v1, v2, v3, v4); + hash += 32; + + hash = MixFinal(hash); + return (int) hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = s_seed + Prime1 + Prime2; + v2 = s_seed + Prime2; + v3 = s_seed; + v4 = s_seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); + + private static uint MixEmptyState() + { + return s_seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + public void Add(T value, IEqualityComparer? comparer) + { + Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + + private void Add(int value) + { + // The original xxHash works as follows: + // 0. Initialize immediately. We can't do this in a struct (no + // default ctor). + // 1. Accumulate blocks of length 16 (4 uints) into 4 accumulators. + // 2. Accumulate remaining blocks of length 4 (1 uint) into the + // hash. + // 3. Accumulate remaining blocks of length 1 into the hash. + + // There is no need for #3 as this type only accepts ints. _queue1, + // _queue2 and _queue3 are basically a buffer so that when + // ToHashCode is called we can execute #2 correctly. + + // We need to initialize the xxHash32 state (_v1 to _v4) lazily (see + // #0) nd the last place that can be done if you look at the + // original code is just before the first block of 16 bytes is mixed + // in. The xxHash32 state is never used for streams containing fewer + // than 16 bytes. + + // To see what's really going on here, have a look at the Combine + // methods. + + uint val = (uint) value; + + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint previousLength = _length++; + uint position = previousLength % 4; + + // Switch can't be inlined. + + if (position == 0) + { + _queue1 = val; + } + else if (position == 1) + { + _queue2 = val; + } + else if (position == 2) + { + _queue3 = val; + } + else // position == 3 + { + if (previousLength == 3) + Initialize(out _v1, out _v2, out _v3, out _v4); + + _v1 = Round(_v1, _queue1); + _v2 = Round(_v2, _queue2); + _v3 = Round(_v3, _queue3); + _v4 = Round(_v4, val); + } + } + + public readonly int ToHashCode() + { + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint length = _length; + + // position refers to the *next* queue position in this method, so + // position == 1 means that _queue1 is populated; _queue2 would have + // been populated on the next call to Add. + uint position = length % 4; + + // If the length is less than 4, _v1 to _v4 don't contain anything + // yet. xxHash32 treats this differently. + + uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + + // _length is incremented once per Add(Int32) and is therefore 4 + // times too small (xxHash length is in bytes, not ints). + + hash += length * 4; + + // Mix what remains in the queue + + // Switch can't be inlined right now, so use as few branches as + // possible by manually excluding impossible scenarios (position > 1 + // is always false if position is not > 0). + if (position > 0) + { + hash = QueueRound(hash, _queue1); + if (position > 1) + { + hash = QueueRound(hash, _queue2); + if (position > 2) + hash = QueueRound(hash, _queue3); + } + } + + hash = MixFinal(hash); + return (int) hash; + } + +#pragma warning disable 0809 + // Obsolete member 'memberA' overrides non-obsolete member 'memberB'. + // Disallowing GetHashCode and Equals is by design + + // * We decided to not override GetHashCode() to produce the hash code + // as this would be weird, both naming-wise as well as from a + // behavioral standpoint (GetHashCode() should return the object's + // hash code, not the one being computed). + + // * Even though ToHashCode() can be called safely multiple times on + // this implementation, it is not part of the contract. If the + // implementation has to change in the future we don't want to worry + // about people who might have incorrectly used this type. + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly override int GetHashCode() => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code."); + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly override bool Equals(object? obj) => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes."); +#pragma warning restore 0809 + } +} diff --git a/Tsu.TreeSourceGen/src/Shims/IsExternalInit.cs b/Tsu.TreeSourceGen/src/Shims/IsExternalInit.cs new file mode 100644 index 0000000..5779b33 --- /dev/null +++ b/Tsu.TreeSourceGen/src/Shims/IsExternalInit.cs @@ -0,0 +1,6 @@ +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit + { + } +} \ No newline at end of file diff --git a/Tsu.TreeSourceGen/src/TsgDiagnostics.cs b/Tsu.TreeSourceGen/src/TsgDiagnostics.cs new file mode 100644 index 0000000..c4bcc0f --- /dev/null +++ b/Tsu.TreeSourceGen/src/TsgDiagnostics.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; + +namespace Tsu.TreeSourceGen; + +internal static class TsgDiagnostics +{ + public static DiagnosticDescriptor NodeDoesNotInheritFromRoot = new( + id: "TSG0001", + title: "Node does not inherit from root", + messageFormat: "The node '{0}' does not inherit from the root type that was listed in the TreeNode attribute", + category: "Tsu.TreeSourceGen", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + customTags: [WellKnownDiagnosticTags.NotConfigurable] + ); + + + public static DiagnosticDescriptor NodeIsNotPartial = new( + id: "TSG0002", + title: "Node is not implemented as a partial class", + messageFormat: "The node '{0}' is not implemented as a partial class. This means that code cannot be generated to extend the class with visitor features.", + category: "Tsu.TreeSourceGen", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + customTags: [WellKnownDiagnosticTags.NotConfigurable] + ); + + public static Diagnostic Create(DiagnosticDescriptor descriptor, ISymbol symbol, params object[] messageArgs) + { + var locations = symbol.DeclaringSyntaxReferences.Select(s => s.SyntaxTree.GetLocation(s.Span)); + return Diagnostic.Create( + descriptor: descriptor, + location: locations.First(), + additionalLocations: locations.Skip(1), + messageArgs: messageArgs); + } +} \ No newline at end of file diff --git a/Tsu.TreeSourceGen/src/Tsu.TreeSourceGen.csproj b/Tsu.TreeSourceGen/src/Tsu.TreeSourceGen.csproj new file mode 100644 index 0000000..18d336e --- /dev/null +++ b/Tsu.TreeSourceGen/src/Tsu.TreeSourceGen.csproj @@ -0,0 +1,24 @@ + + + + SourceGenerator + + netstandard2.0 + enable + enable + preview + false + + true + true + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/Tsu.sln b/Tsu.sln index 82fd73e..2145aec 100644 --- a/Tsu.sln +++ b/Tsu.sln @@ -53,6 +53,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tsu.Timing.MicroProfiler", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tsu.Windows", "Tsu.Windows\Tsu.Windows.csproj", "{D673B185-360D-4620-92C2-1F0089C6C4FB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tsu.TreeSourceGen", "Tsu.TreeSourceGen", "{8D4425EC-0DC5-45D0-868F-E8BAEFD0A7C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tsu.TreeSourceGen", "Tsu.TreeSourceGen\src\Tsu.TreeSourceGen.csproj", "{4C138EF8-296A-4CD7-8F7E-2D14C91D30F1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,6 +127,10 @@ Global {D673B185-360D-4620-92C2-1F0089C6C4FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {D673B185-360D-4620-92C2-1F0089C6C4FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {D673B185-360D-4620-92C2-1F0089C6C4FB}.Release|Any CPU.Build.0 = Release|Any CPU + {4C138EF8-296A-4CD7-8F7E-2D14C91D30F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C138EF8-296A-4CD7-8F7E-2D14C91D30F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C138EF8-296A-4CD7-8F7E-2D14C91D30F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C138EF8-296A-4CD7-8F7E-2D14C91D30F1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -143,6 +151,7 @@ Global {9D81E6F2-3512-48AD-8088-9A2F4E2089F6} = {35960E56-949E-4547-B998-343933E70C6D} {D591FE5A-55F9-47F3-B67B-5656CB7F45E5} = {9A94287D-D6A5-4A40-AE4A-171047B283A0} {B5337497-B391-42F8-80D1-7169F3053B77} = {0E4D8A6A-8EBC-431F-935C-5A97D5B96280} + {4C138EF8-296A-4CD7-8F7E-2D14C91D30F1} = {8D4425EC-0DC5-45D0-868F-E8BAEFD0A7C6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B8BE0376-954A-4E0C-BBD8-5B5F8E17634C}