|
| 1 | +using System.Text; |
| 2 | + |
| 3 | +using Microsoft.CodeAnalysis; |
| 4 | +using Microsoft.CodeAnalysis.CSharp; |
| 5 | +using Microsoft.CodeAnalysis.CSharp.Syntax; |
| 6 | +using Microsoft.CodeAnalysis.Text; |
| 7 | + |
| 8 | +namespace F1Game.UDP.SourceGenerator; |
| 9 | + |
| 10 | +[Generator] |
| 11 | +sealed class AutoInlineArrayGenerator : IIncrementalGenerator |
| 12 | +{ |
| 13 | + public void Initialize(IncrementalGeneratorInitializationContext context) |
| 14 | + { |
| 15 | + context.RegisterPostInitializationOutput(AutoInlineArrayAttributeSource.AddAttributeSource); |
| 16 | + |
| 17 | + IncrementalValuesProvider<BuildTarget> buildTargets = context.SyntaxProvider.ForAttributeWithMetadataName( |
| 18 | + AutoInlineArrayAttributeSource.FullyQualifiedAutoInlineArrayAttribute, |
| 19 | + predicate: static (node, _) => NodePredicate(node), |
| 20 | + transform: static (ctx, ct) => GetSemanticTargetForGeneration(ctx, ct)) |
| 21 | + .Where(static m => m is not null)!; |
| 22 | + |
| 23 | + context.RegisterSourceOutput(buildTargets, static (context, source) => ExecuteGeneration(context, source)); |
| 24 | + } |
| 25 | + |
| 26 | + static bool NodePredicate(SyntaxNode node) |
| 27 | + { |
| 28 | + return node is TypeDeclarationSyntax { Keyword.RawKind: (int)SyntaxKind.StructKeyword } |
| 29 | + || node is RecordDeclarationSyntax { ClassOrStructKeyword.RawKind: (int)SyntaxKind.StructKeyword }; |
| 30 | + } |
| 31 | + |
| 32 | + static BuildTarget? GetSemanticTargetForGeneration(GeneratorAttributeSyntaxContext context, CancellationToken _) |
| 33 | + { |
| 34 | + if (context.TargetSymbol is not INamedTypeSymbol type) |
| 35 | + return null; |
| 36 | + |
| 37 | + if (!type.DeclaringSyntaxReferences.Select(x => x.GetSyntax()).Any(x => x is TypeDeclarationSyntax typeSyntax && typeSyntax.Modifiers.Any(SyntaxKind.PartialKeyword))) |
| 38 | + return Diagnostic.Create(DiagnosticDescriptors.TypeMustBePartial, type.Locations[0], type.Name); |
| 39 | + |
| 40 | + if (context.Attributes.Length > 1) |
| 41 | + return Diagnostic.Create(DiagnosticDescriptors.NoMoreThanOneAttribute, type.Locations[0], type.Name); |
| 42 | + |
| 43 | + if (context.Attributes[0] is not { } attributeData) |
| 44 | + return null; |
| 45 | + |
| 46 | + if (attributeData.ConstructorArguments[0].Value is not int length || length <= 0) |
| 47 | + return Diagnostic.Create(DiagnosticDescriptors.LengthShouldBePositiveNumber, type.Locations[0], type.Name); |
| 48 | + |
| 49 | + var elementTypeFromAttribute = attributeData.ConstructorArguments.Length > 1 |
| 50 | + ? attributeData.ConstructorArguments[1].Value as ITypeSymbol |
| 51 | + : null; |
| 52 | + |
| 53 | + var instanceFields = type.GetMembers() |
| 54 | + .OfType<IFieldSymbol>() |
| 55 | + .Where(f => !f.IsStatic && !f.IsConst) |
| 56 | + .ToArray(); |
| 57 | + |
| 58 | + if (instanceFields.Length > 1) |
| 59 | + return Diagnostic.Create(DiagnosticDescriptors.TooManyInstanceFields, type.Locations[0], type.Name); |
| 60 | + |
| 61 | + return (instanceFields.FirstOrDefault(), elementTypeFromAttribute) switch |
| 62 | + { |
| 63 | + (null, null) when type.IsGenericType && type.TypeParameters.Length > 0 => new GenerationData(type, length, type.TypeParameters[0], true), |
| 64 | + (null, { } elementType) => new GenerationData(type, length, elementType, true), |
| 65 | + ({ } field, null) => new GenerationData(type, length, field.Type, false), |
| 66 | + ({ }, { }) => Diagnostic.Create(DiagnosticDescriptors.InstanceFieldAndElementTypeArePresent, type.Locations[0], type.Name), |
| 67 | + _ => Diagnostic.Create(DiagnosticDescriptors.NoTypeForInstanceField, type.Locations[0], type.Name), |
| 68 | + }; |
| 69 | + } |
| 70 | + |
| 71 | + static void ExecuteGeneration(SourceProductionContext context, BuildTarget target) |
| 72 | + { |
| 73 | + context.CancellationToken.ThrowIfCancellationRequested(); |
| 74 | + |
| 75 | + if (target.Diagnostic is not null) |
| 76 | + { |
| 77 | + context.ReportDiagnostic(target.Diagnostic); |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + if (target.GenerationData is not GenerationData generationData) |
| 82 | + return; |
| 83 | + |
| 84 | + var (typeData, length, elementType, generateField) = generationData; |
| 85 | + var (namespaceName, typeName, typeDeclaration, typeMetadataName) = typeData; |
| 86 | + var namespaceDeclaration = namespaceName is null ? "" : $"namespace {namespaceName};"; |
| 87 | + |
| 88 | + string fieldGenerationCode = generateField |
| 89 | + ? $$""" |
| 90 | + [SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Inline array element field must be mutable.")] |
| 91 | + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Field is used by the runtime for InlineArray layout and access.")] |
| 92 | + private {{elementType}} _element0; |
| 93 | + """ |
| 94 | + : ""; |
| 95 | + |
| 96 | + string source = $$""" |
| 97 | + // <auto-generated/> |
| 98 | + #nullable enable |
| 99 | +
|
| 100 | + using System; |
| 101 | + using System.Collections.Generic; |
| 102 | + using System.Diagnostics.CodeAnalysis; |
| 103 | + using System.Runtime.CompilerServices; |
| 104 | +
|
| 105 | + {{namespaceDeclaration}} |
| 106 | +
|
| 107 | + /// <summary> |
| 108 | + /// Represents an inline array {{typeName}} with {{length}} elements of type <see cref="{{elementType}}"/>. |
| 109 | + /// Provides basic equality comparison and hashing. Access elements using the indexer (e.g., myArray[0]). |
| 110 | + /// </summary> |
| 111 | + [InlineArray({{length}})] |
| 112 | + partial {{typeDeclaration}} {{typeName}} |
| 113 | + : IEquatable<{{typeName}}> |
| 114 | + { |
| 115 | + {{fieldGenerationCode}} |
| 116 | +
|
| 117 | + /// <summary>Gets the fixed length of the inline array: {{length}}.</summary> |
| 118 | + public int Length => {{length}}; |
| 119 | +
|
| 120 | + /// <summary> |
| 121 | + /// Returns a <see cref="Span{T}"/> that represents the elements of this inline array. |
| 122 | + /// </summary> |
| 123 | + /// <returns> |
| 124 | + /// A <see cref="Span{T}"/> of length <c>{{length}}</c> that provides mutable access to the elements of the inline array. |
| 125 | + /// </returns> |
| 126 | + public Span<{{elementType}}> AsSpan() |
| 127 | + => MemoryMarshal.CreateSpan(ref Unsafe.As<{{typeName}}, {{elementType}}>(ref this), {{length}}); |
| 128 | +
|
| 129 | + /// <summary> |
| 130 | + /// Returns a <see cref="ReadOnlySpan{T}"/> that represents the elements of this inline array. |
| 131 | + /// </summary> |
| 132 | + /// <returns> |
| 133 | + /// A <see cref="ReadOnlySpan{T}"/> of length <c>{{length}}</c> that provides read-only access to the elements of the inline array. |
| 134 | + /// </returns> |
| 135 | + public ReadOnlySpan<{{elementType}}> AsReadOnlySpan() |
| 136 | + => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<{{typeName}}, {{elementType}}>(ref this), {{length}}); |
| 137 | +
|
| 138 | + /// <summary> |
| 139 | + /// Returns an <see cref="IEnumerable{T}"/> that enumerates the elements of this inline array. |
| 140 | + /// </summary> |
| 141 | + /// <returns> |
| 142 | + /// An <see cref="IEnumerable{T}"/> that iterates over the elements of the inline array in order. |
| 143 | + /// </returns> |
| 144 | + public IEnumerable<{{elementType}}> AsEnumerable() |
| 145 | + { |
| 146 | + foreach (var item in this) |
| 147 | + yield return item; |
| 148 | + } |
| 149 | +
|
| 150 | + /// <inheritdoc/> |
| 151 | + public override bool Equals([NotNullWhen(true)] object? obj) |
| 152 | + => obj is {{typeName}} other && Equals(other); |
| 153 | +
|
| 154 | + /// <summary>Indicates whether the current inline array is equal to another inline array of the same type by comparing their elements sequentially.</summary> |
| 155 | + /// <param name="other">An inline array to compare with this instance.</param> |
| 156 | + /// <returns><c>true</c> if the current array's elements are equal to the <paramref name="other"/> array's elements; otherwise, <c>false</c>.</returns> |
| 157 | + public bool Equals({{typeName}} other) |
| 158 | + => ((ReadOnlySpan<{{elementType}}>)this).SequenceEqual((ReadOnlySpan<{{elementType}}>)other); |
| 159 | +
|
| 160 | + /// <inheritdoc/> |
| 161 | + public override int GetHashCode() |
| 162 | + { |
| 163 | + HashCode hashCode = default; |
| 164 | + ReadOnlySpan<{{elementType}}> span = this; |
| 165 | + var comparer = EqualityComparer<{{elementType}}>.Default; |
| 166 | +
|
| 167 | + foreach (ref readonly {{elementType}} item in span) |
| 168 | + hashCode.Add(item, comparer); |
| 169 | +
|
| 170 | + return hashCode.ToHashCode(); |
| 171 | + } |
| 172 | +
|
| 173 | + /// <summary> |
| 174 | + /// Creates a new <see cref="{{typeName}}"/> instance and populates it with elements from the specified <see cref="ReadOnlySpan{T}"/> source. |
| 175 | + /// </summary> |
| 176 | + /// <param name="source"> |
| 177 | + /// The sequence of elements to copy into the inline array. If the sequence contains fewer elements than the array's length, the remaining elements are left at their default value. |
| 178 | + /// If the sequence contains more elements than the array's length, the extra elements are ignored. |
| 179 | + /// </param> |
| 180 | + /// <returns> |
| 181 | + /// A new <see cref="{{typeName}}"/> instance containing elements from <paramref name="source"/>. |
| 182 | + /// </returns> |
| 183 | + public static {{typeName}} Create(ReadOnlySpan<{{elementType}}> source) |
| 184 | + { |
| 185 | + var array = new {{typeName}}(); |
| 186 | + for (var i = 0; i < array.Length && i < source.Length; i++) |
| 187 | + array[i] = source[i]; |
| 188 | + return array; |
| 189 | + } |
| 190 | +
|
| 191 | + /// <summary>Determines whether two specified instances of <see cref="{{typeName}}"/> are equal by comparing their elements sequence.</summary> |
| 192 | + /// <param name="left">The first inline array to compare.</param> |
| 193 | + /// <param name="right">The second inline array to compare.</param> |
| 194 | + /// <returns><c>true</c> if the arrays are equal; otherwise, <c>false</c>.</returns> |
| 195 | + public static bool operator ==({{typeName}} left, {{typeName}} right) |
| 196 | + => left.Equals(right); |
| 197 | +
|
| 198 | + /// <summary>Determines whether two specified instances of <see cref="{{typeName}}"/> are not equal by comparing their elements sequence.</summary> |
| 199 | + /// <param name="left">The first inline array to compare.</param> |
| 200 | + /// <param name="right">The second inline array to compare.</param> |
| 201 | + /// <returns><c>true</c> if the arrays are not equal; otherwise, <c>false</c>.</returns> |
| 202 | + public static bool operator !=({{typeName}} left, {{typeName}} right) |
| 203 | + => !(left == right); |
| 204 | +
|
| 205 | + /// <summary> |
| 206 | + /// Implicitly converts an array of <see cref="{{elementType}}"/> to a <see cref="{{typeName}}"/>. |
| 207 | + /// Copies up to <c>{{length}}</c> elements from the source array; extra elements are ignored, and missing elements are default-initialized. |
| 208 | + /// </summary> |
| 209 | + /// <param name="source"> |
| 210 | + /// The source array to copy elements from. If <paramref name="source"/> is <c>null</c>, a default-initialized <see cref="{{typeName}}"/> is returned. |
| 211 | + /// </param> |
| 212 | + /// <returns> |
| 213 | + /// A <see cref="{{typeName}}"/> containing elements from <paramref name="source"/>. |
| 214 | + /// </returns> |
| 215 | + public static implicit operator {{typeName}}({{elementType}}[] source) |
| 216 | + => source is null ? new() : Create(source); |
| 217 | +
|
| 218 | + /// <summary> |
| 219 | + /// Implicitly converts a <see cref="ReadOnlySpan{T}"/> of <see cref="{{elementType}}"/> to a <see cref="{{typeName}}"/>. |
| 220 | + /// Copies up to <c>{{length}}</c> elements from the source span; extra elements are ignored, and missing elements are default-initialized. |
| 221 | + /// </summary> |
| 222 | + /// <param name="source"> |
| 223 | + /// The source span to copy elements from. |
| 224 | + /// </param> |
| 225 | + /// <returns> |
| 226 | + /// A <see cref="{{typeName}}"/> containing elements from <paramref name="source"/>. |
| 227 | + /// </returns> |
| 228 | + public static implicit operator {{typeName}}(ReadOnlySpan<{{elementType}}> source) |
| 229 | + => Create(source); |
| 230 | +
|
| 231 | + /// <summary> |
| 232 | + /// Implicitly converts a <see cref="Span{T}"/> of <see cref="{{elementType}}"/> to a <see cref="{{typeName}}"/>. |
| 233 | + /// Copies up to <c>{{length}}</c> elements from the source span; extra elements are ignored, and missing elements are default-initialized. |
| 234 | + /// </summary> |
| 235 | + /// <param name="source"> |
| 236 | + /// The source span to copy elements from. |
| 237 | + /// </param> |
| 238 | + /// <returns> |
| 239 | + /// A <see cref="{{typeName}}"/> containing elements from <paramref name="source"/>. |
| 240 | + /// </returns> |
| 241 | + public static implicit operator {{typeName}}(Span<{{elementType}}> source) |
| 242 | + => Create(source); |
| 243 | + } |
| 244 | + """; |
| 245 | + |
| 246 | + context.AddSource($"{typeMetadataName}.Generated.cs", SourceText.From(source, Encoding.UTF8)); |
| 247 | + } |
| 248 | +} |
0 commit comments