EnumExtensionMethodsGenerationInfo.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Immutable;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Linq;
  6. using System.Threading;
  7. using JetBrains.Annotations;
  8. using Microsoft.CodeAnalysis;
  9. using Terminal.Gui.Analyzers.Internal.Attributes;
  10. using Terminal.Gui.Analyzers.Internal.Constants;
  11. namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions;
  12. /// <summary>
  13. /// Type containing the information necessary to generate code according to the declared attribute values,
  14. /// as well as the actual code to create the corresponding source code text, to be used in the
  15. /// source generator pipeline.
  16. /// </summary>
  17. /// <remarks>
  18. /// Minimal validation is performed by this type.<br/>
  19. /// Errors in analyzed source code will result in generation failure or broken output.<br/>
  20. /// This type is not intended for use outside of Terminal.Gui library development.
  21. /// </remarks>
  22. internal sealed record EnumExtensionMethodsGenerationInfo : IGeneratedTypeMetadata<EnumExtensionMethodsGenerationInfo>,
  23. IEqualityOperators<EnumExtensionMethodsGenerationInfo, EnumExtensionMethodsGenerationInfo, bool>
  24. {
  25. private const int ExplicitFastHasFlagsMask = 0b_0100;
  26. private const int ExplicitFastIsDefinedMask = 0b_1000;
  27. private const int ExplicitNameMask = 0b_0010;
  28. private const int ExplicitNamespaceMask = 0b_0001;
  29. private const string GeneratorAttributeFullyQualifiedName = $"{GeneratorAttributeNamespace}.{GeneratorAttributeName}";
  30. private const string GeneratorAttributeName = nameof (GenerateEnumExtensionMethodsAttribute);
  31. private const string GeneratorAttributeNamespace = Strings.AnalyzersAttributesNamespace;
  32. /// <summary>
  33. /// Type containing the information necessary to generate code according to the declared attribute values,
  34. /// as well as the actual code to create the corresponding source code text, to be used in the
  35. /// source generator pipeline.
  36. /// </summary>
  37. /// <param name="enumNamespace">The fully-qualified namespace of the enum type, without assembly name.</param>
  38. /// <param name="enumTypeName">
  39. /// The name of the enum type, as would be given by <see langword="nameof"/> on the enum's type
  40. /// declaration.
  41. /// </param>
  42. /// <param name="typeNamespace">
  43. /// The fully-qualified namespace in which to place the generated code, without assembly name. If omitted or explicitly
  44. /// null, uses the value provided in <paramref name="enumNamespace"/>.
  45. /// </param>
  46. /// <param name="typeName">
  47. /// The name of the generated class. If omitted or explicitly null, appends "Extensions" to the value of
  48. /// <paramref name="enumTypeName"/>.
  49. /// </param>
  50. /// <param name="enumBackingTypeCode">The backing type of the enum. Defaults to <see cref="int"/>.</param>
  51. /// <param name="generateFastHasFlags">
  52. /// Whether to generate a fast HasFlag alternative. (Default: true) Ignored if the enum does not also have
  53. /// <see cref="FlagsAttribute"/>.
  54. /// </param>
  55. /// <param name="generateFastIsDefined">Whether to generate a fast IsDefined alternative. (Default: true)</param>
  56. /// <remarks>
  57. /// Minimal validation is performed by this type.<br/>
  58. /// Errors in analyzed source code will result in generation failure or broken output.<br/>
  59. /// This type is not intended for use outside of Terminal.Gui library development.
  60. /// </remarks>
  61. public EnumExtensionMethodsGenerationInfo (
  62. string enumNamespace,
  63. string enumTypeName,
  64. string? typeNamespace = null,
  65. string? typeName = null,
  66. TypeCode enumBackingTypeCode = TypeCode.Int32,
  67. bool generateFastHasFlags = true,
  68. bool generateFastIsDefined = true
  69. ) : this (enumNamespace, enumTypeName, enumBackingTypeCode)
  70. {
  71. GeneratedTypeNamespace = typeNamespace ?? enumNamespace;
  72. GeneratedTypeName = typeName ?? string.Concat (enumTypeName, Strings.DefaultTypeNameSuffix);
  73. GenerateFastHasFlags = generateFastHasFlags;
  74. GenerateFastIsDefined = generateFastIsDefined;
  75. }
  76. public EnumExtensionMethodsGenerationInfo (string enumNamespace, string enumTypeName, TypeCode enumBackingType)
  77. {
  78. // Interning these since they're rather unlikely to change.
  79. string enumInternedNamespace = string.Intern (enumNamespace);
  80. string enumInternedName = string.Intern (enumTypeName);
  81. TargetTypeNamespace = enumInternedNamespace;
  82. TargetTypeName = enumInternedName;
  83. EnumBackingTypeCode = enumBackingType;
  84. }
  85. [AccessedThroughProperty (nameof (EnumBackingTypeCode))]
  86. private readonly TypeCode _enumBackingTypeCode;
  87. [AccessedThroughProperty (nameof (GeneratedTypeName))]
  88. private string? _generatedTypeName;
  89. [AccessedThroughProperty (nameof (GeneratedTypeNamespace))]
  90. private string? _generatedTypeNamespace;
  91. private BitVector32 _discoveredProperties = new (0);
  92. /// <summary>The name of the extension class.</summary>
  93. public string? GeneratedTypeName
  94. {
  95. get => _generatedTypeName ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
  96. set => _generatedTypeName = value ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
  97. }
  98. /// <summary>The namespace for the extension class.</summary>
  99. /// <remarks>
  100. /// Value is not validated by the set accessor.<br/>
  101. /// Get accessor will never return null and is thus marked [NotNull] for static analysis, even though the property is
  102. /// declared as a nullable <see langword="string?"/>.<br/>If the backing field for this property is null, the get
  103. /// accessor will return <see cref="TargetTypeNamespace"/> instead.
  104. /// </remarks>
  105. public string? GeneratedTypeNamespace
  106. {
  107. get => _generatedTypeNamespace ?? TargetTypeNamespace;
  108. set => _generatedTypeNamespace = value ?? TargetTypeNamespace;
  109. }
  110. /// <inheritdoc/>
  111. public string TargetTypeFullName => string.Concat (TargetTypeNamespace, ".", TargetTypeName);
  112. /// <inheritdoc/>
  113. public Accessibility Accessibility
  114. {
  115. get;
  116. [UsedImplicitly]
  117. internal set;
  118. } = Accessibility.Public;
  119. /// <inheritdoc/>
  120. public TypeKind TypeKind => TypeKind.Class;
  121. /// <inheritdoc/>
  122. public bool IsRecord => false;
  123. /// <inheritdoc/>
  124. public bool IsClass => true;
  125. /// <inheritdoc/>
  126. public bool IsStruct => false;
  127. /// <inheritdoc/>
  128. public bool IsByRefLike => false;
  129. /// <inheritdoc/>
  130. public bool IsSealed => false;
  131. /// <inheritdoc/>
  132. public bool IsAbstract => false;
  133. /// <inheritdoc/>
  134. public bool IsEnum => false;
  135. /// <inheritdoc/>
  136. public bool IsStatic => true;
  137. /// <inheritdoc/>
  138. public bool IncludeInterface => false;
  139. public string GeneratedTypeFullName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}";
  140. /// <summary>Whether to generate the extension class as partial (Default: true)</summary>
  141. public bool IsPartial => true;
  142. /// <summary>The fully-qualified namespace of the source enum type.</summary>
  143. public string TargetTypeNamespace
  144. {
  145. get;
  146. [UsedImplicitly]
  147. set;
  148. }
  149. /// <summary>The UNQUALIFIED name of the source enum type.</summary>
  150. public string TargetTypeName
  151. {
  152. get;
  153. [UsedImplicitly]
  154. set;
  155. }
  156. /// <summary>
  157. /// The backing type for the enum.
  158. /// </summary>
  159. /// <remarks>For simplicity and formality, only System.Int32 and System.UInt32 are supported at this time.</remarks>
  160. public TypeCode EnumBackingTypeCode
  161. {
  162. get => _enumBackingTypeCode;
  163. init
  164. {
  165. if (value is not TypeCode.Int32 and not TypeCode.UInt32)
  166. {
  167. throw new NotSupportedException ("Only System.Int32 and System.UInt32 are supported at this time.");
  168. }
  169. _enumBackingTypeCode = value;
  170. }
  171. }
  172. /// <summary>
  173. /// Whether a fast alternative to the built-in Enum.HasFlag method will be generated (Default: false)
  174. /// </summary>
  175. public bool GenerateFastHasFlags { [UsedImplicitly] get; set; }
  176. /// <summary>Whether a switch-based IsDefined replacement will be generated (Default: true)</summary>
  177. public bool GenerateFastIsDefined { [UsedImplicitly]get; set; } = true;
  178. internal ImmutableHashSet<int>? _intMembers;
  179. internal ImmutableHashSet<uint>? _uIntMembers;
  180. /// <summary>
  181. /// Fully-qualified name of the extension class
  182. /// </summary>
  183. internal string FullyQualifiedClassName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}";
  184. /// <summary>
  185. /// Whether a Flags was found on the enum type.
  186. /// </summary>
  187. internal bool HasFlagsAttribute {[UsedImplicitly] get; set; }
  188. private static readonly SymbolDisplayFormat FullyQualifiedSymbolDisplayFormatWithoutGlobal =
  189. SymbolDisplayFormat.FullyQualifiedFormat
  190. .WithGlobalNamespaceStyle (
  191. SymbolDisplayGlobalNamespaceStyle.Omitted);
  192. internal bool TryConfigure (INamedTypeSymbol enumSymbol, CancellationToken cancellationToken)
  193. {
  194. using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
  195. cts.Token.ThrowIfCancellationRequested ();
  196. ImmutableArray<AttributeData> attributes = enumSymbol.GetAttributes ();
  197. // This is theoretically impossible, but guarding just in case and canceling if it does happen.
  198. if (attributes.Length == 0)
  199. {
  200. cts.Cancel (true);
  201. return false;
  202. }
  203. // Check all attributes provided for anything interesting.
  204. // Attributes can be in any order, so just check them all and adjust at the end if necessary.
  205. // Note that we do not perform as strict validation on actual usage of the attribute, at this stage,
  206. // because the analyzer should have already thrown errors for invalid uses like global namespace
  207. // or unsupported enum underlying types.
  208. foreach (AttributeData attr in attributes)
  209. {
  210. cts.Token.ThrowIfCancellationRequested ();
  211. string? attributeFullyQualifiedName = attr.AttributeClass?.ToDisplayString (FullyQualifiedSymbolDisplayFormatWithoutGlobal);
  212. // Skip if null or not possibly an attribute we care about
  213. if (attributeFullyQualifiedName is null or not { Length: >= 5 })
  214. {
  215. continue;
  216. }
  217. switch (attributeFullyQualifiedName)
  218. {
  219. // For Flags enums
  220. case Strings.DotnetNames.Attributes.Flags:
  221. {
  222. HasFlagsAttribute = true;
  223. }
  224. continue;
  225. // For the attribute that started this whole thing
  226. case GeneratorAttributeFullyQualifiedName:
  227. {
  228. // If we can't successfully complete this method,
  229. // something is wrong enough that we may as well just stop now.
  230. if (!TryConfigure (attr, cts.Token))
  231. {
  232. if (cts.Token.CanBeCanceled)
  233. {
  234. cts.Cancel ();
  235. }
  236. return false;
  237. }
  238. }
  239. continue;
  240. }
  241. }
  242. // Now get the members, if we know we'll need them.
  243. if (GenerateFastIsDefined || GenerateFastHasFlags)
  244. {
  245. if (EnumBackingTypeCode == TypeCode.Int32)
  246. {
  247. PopulateIntMembersHashSet (enumSymbol);
  248. }
  249. else if (EnumBackingTypeCode == TypeCode.UInt32)
  250. {
  251. PopulateUIntMembersHashSet (enumSymbol);
  252. }
  253. }
  254. return true;
  255. }
  256. private void PopulateIntMembersHashSet (INamedTypeSymbol enumSymbol)
  257. {
  258. ImmutableArray<ISymbol> enumMembers = enumSymbol.GetMembers ();
  259. IEnumerable<IFieldSymbol> fieldSymbols = enumMembers.OfType<IFieldSymbol> ();
  260. _intMembers = fieldSymbols.Select (static m => m.HasConstantValue ? (int)m.ConstantValue : 0).ToImmutableHashSet ();
  261. }
  262. private void PopulateUIntMembersHashSet (INamedTypeSymbol enumSymbol)
  263. {
  264. _uIntMembers = enumSymbol.GetMembers ().OfType<IFieldSymbol> ().Select (static m => (uint)m.ConstantValue).ToImmutableHashSet ();
  265. }
  266. private bool HasExplicitFastHasFlags
  267. {
  268. [UsedImplicitly]get => _discoveredProperties [ExplicitFastHasFlagsMask];
  269. set => _discoveredProperties [ExplicitFastHasFlagsMask] = value;
  270. }
  271. private bool HasExplicitFastIsDefined
  272. {
  273. [UsedImplicitly]get => _discoveredProperties [ExplicitFastIsDefinedMask];
  274. set => _discoveredProperties [ExplicitFastIsDefinedMask] = value;
  275. }
  276. private bool HasExplicitTypeName
  277. {
  278. get => _discoveredProperties [ExplicitNameMask];
  279. set => _discoveredProperties [ExplicitNameMask] = value;
  280. }
  281. private bool HasExplicitTypeNamespace
  282. {
  283. get => _discoveredProperties [ExplicitNamespaceMask];
  284. set => _discoveredProperties [ExplicitNamespaceMask] = value;
  285. }
  286. [MemberNotNullWhen (true, nameof (_generatedTypeName), nameof (_generatedTypeNamespace))]
  287. private bool TryConfigure (AttributeData attr, CancellationToken cancellationToken)
  288. {
  289. using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
  290. cts.Token.ThrowIfCancellationRequested ();
  291. if (attr is not { NamedArguments.Length: > 0 })
  292. {
  293. // Just a naked attribute, so configure with appropriate defaults.
  294. GeneratedTypeNamespace = TargetTypeNamespace;
  295. GeneratedTypeName = string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
  296. return true;
  297. }
  298. cts.Token.ThrowIfCancellationRequested ();
  299. foreach (KeyValuePair<string, TypedConstant> kvp in attr.NamedArguments)
  300. {
  301. string? propName = kvp.Key;
  302. TypedConstant propValue = kvp.Value;
  303. cts.Token.ThrowIfCancellationRequested ();
  304. // For every property name and value pair, set associated metadata
  305. // property, if understood.
  306. switch (propName, propValue)
  307. {
  308. // Null or empty string doesn't make sense, so skip if it happens.
  309. case (null, _):
  310. case ("", _):
  311. continue;
  312. // ClassName is specified, not explicitly null, and at least 1 character long.
  313. case (AttributeProperties.TypeNamePropertyName, { IsNull: false, Value: string { Length: > 1 } classNameProvidedValue }):
  314. if (string.IsNullOrWhiteSpace (classNameProvidedValue))
  315. {
  316. return false;
  317. }
  318. GeneratedTypeName = classNameProvidedValue;
  319. HasExplicitTypeName = true;
  320. continue;
  321. // Class namespace is specified, not explicitly null, and at least 1 character long.
  322. case (AttributeProperties.TypeNamespacePropertyName, { IsNull: false, Value: string { Length: > 1 } classNamespaceProvidedValue }):
  323. if (string.IsNullOrWhiteSpace (classNamespaceProvidedValue))
  324. {
  325. return false;
  326. }
  327. GeneratedTypeNamespace = classNamespaceProvidedValue;
  328. HasExplicitTypeNamespace = true;
  329. continue;
  330. // FastHasFlags is specified
  331. case (AttributeProperties.FastHasFlagsPropertyName, { IsNull: false } fastHasFlagsConstant):
  332. GenerateFastHasFlags = fastHasFlagsConstant.Value is true;
  333. HasExplicitFastHasFlags = true;
  334. continue;
  335. // FastIsDefined is specified
  336. case (AttributeProperties.FastIsDefinedPropertyName, { IsNull: false } fastIsDefinedConstant):
  337. GenerateFastIsDefined = fastIsDefinedConstant.Value is true;
  338. HasExplicitFastIsDefined = true;
  339. continue;
  340. }
  341. }
  342. // The rest is simple enough it's not really worth worrying about cancellation, so don't bother from here on...
  343. // Configure anything that wasn't specified that doesn't have an implicitly safe default
  344. if (!HasExplicitTypeName || _generatedTypeName is null)
  345. {
  346. _generatedTypeName = string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
  347. }
  348. if (!HasExplicitTypeNamespace || _generatedTypeNamespace is null)
  349. {
  350. _generatedTypeNamespace = TargetTypeNamespace;
  351. }
  352. if (!HasFlagsAttribute)
  353. {
  354. GenerateFastHasFlags = false;
  355. }
  356. return true;
  357. }
  358. private static class AttributeProperties
  359. {
  360. internal const string FastHasFlagsPropertyName = nameof (GenerateEnumExtensionMethodsAttribute.FastHasFlags);
  361. internal const string FastIsDefinedPropertyName = nameof (GenerateEnumExtensionMethodsAttribute.FastIsDefined);
  362. internal const string TypeNamePropertyName = nameof (GenerateEnumExtensionMethodsAttribute.ClassName);
  363. internal const string TypeNamespacePropertyName = nameof (GenerateEnumExtensionMethodsAttribute.ClassNamespace);
  364. }
  365. }