EnumExtensionMethodsGenerationInfo.cs 18 KB

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