DeprecatedDuplicateBuilder.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT License.
  3. // See the LICENSE file in the project root for more information.
  4. using Microsoft.CodeAnalysis;
  5. using Microsoft.CodeAnalysis.CSharp;
  6. using Microsoft.CodeAnalysis.CSharp.Syntax;
  7. namespace System.Linq.Async.SourceGenerator
  8. {
  9. /// <summary>
  10. /// Builds a facade class containing duplicate definitions of the various deprecated AsyncEnumerable methods
  11. /// as members of <c>AsyncEnumerableDeprecated</c>.
  12. /// </summary>
  13. /// <remarks>
  14. /// <para>
  15. /// This is necessary because of complicated backwards compatibility issues.
  16. /// </para>
  17. /// <para>
  18. /// The scenario this addresses is when an application has been using System.Linq.Async v6, and was using
  19. /// methods that have been marked as obsolete in System.Linq.Async v7. For example, code might be using
  20. /// the <c>WhereAwaitWithCancellation</c> extension method.
  21. /// </para>
  22. /// <para>
  23. /// When the developer upgrades such a project to System.Linq.Async v7, we want to ensure that they get
  24. /// deprecation warnings telling them which method they should use instead (e.g., <c>WhereAwaitWithCancellation</c>
  25. /// should be replaced with one of the overloads of <c>Where</c> that .NET 10's <c>System.Linq.AsyncEnumerable</c>
  26. /// defines. However, we don't want to break their build immediately; if they are OK with the deprecation warnings,
  27. /// we want their existing code to continue to work without changes.
  28. /// </para>
  29. /// <para>
  30. /// This is complicated by the fact that in .NET 10, <c>System.Linq.AsyncEnumerable</c> now defines its own
  31. /// <c>AsyncEnumerable</c> type, meaning that <c>System.Linq.Async</c>'s public API must no longer contain a type
  32. /// of that name. (Otherwise, code such as <c>AsyncEnumerable.Range(1, 10)</c> would fail to compile due to the
  33. /// type name being ambiguous.) Thus, the public-facing API of <c>System.Linq.Async</c> in v7 has moved all of the
  34. /// extension methods that it continues to define into a type named <c>AsyncEnumerableDeprecated</c>. (We've
  35. /// called it this because the only methods we need to retain from the public API are the ones that are deprecated.
  36. /// In cases where there are direct replacements in .NET 10's <c>System.Linq.AsyncEnumerable</c>, we've removed
  37. /// the corresponding methods from <c>System.Linq.Async</c> entirely. And in other cases we've moved functionality
  38. /// into <c>System.Interactive.Async</c>. The goal is for everyone to stop using <c>System.Linq.Async</c>, so by
  39. /// definition, if you are still using a method it defines, that method is obsolete.)
  40. /// </para>
  41. /// <para>
  42. /// But now the problem is that code that continues to use these deprecated methods will expect them to live in
  43. /// <c>AsyncEnumerableDeprecated</c>, because that's where the compiler will find the relevant methods. So
  44. /// the runtime assembly will need to make these methods available in a type of that name. But for binary backwards
  45. /// compatibility, we need every method that our <c>AsyncEnumerable</c> defined in V6 still to be available on
  46. /// a type still called <c>AsyncEnumerable</c>.
  47. /// </para>
  48. /// <para>
  49. /// In other words, we need to make our API available twice, on two different types. One for code that hasn't been
  50. /// recompiled against v7, and therefore expects all the methods to be in <c>AsyncEnumerable</c>, and one for code
  51. /// that has been recompiled against v7 but which has chosen to continue using deprecated method, and which will
  52. /// expect those to be in <c>AsyncEnumerableDeprecated</c>.
  53. /// </para>
  54. /// <para>
  55. /// So this generator duplicates public static methods defined by <c>AsyncEnumerable</c> into <c>AsyncEnumerableDeprecated</c>.
  56. /// It also strips off their extension method 'this' modifier from the first parameter, so that they are normal static methods,
  57. /// because otherwise, we get ambiguity errors in our unit tests.
  58. /// </para>
  59. /// </remarks>
  60. internal class DeprecatedDuplicateBuilder
  61. {
  62. private readonly GeneratorExecutionContext _context;
  63. private readonly GenerationOptions _options;
  64. private readonly INamedTypeSymbol _generateAsyncOverloadAttributeAttributeSymbol;
  65. private readonly INamedTypeSymbol _attributeSymbol;
  66. private readonly SyntaxReceiver _syntaxReceiver;
  67. public DeprecatedDuplicateBuilder(
  68. GeneratorExecutionContext context,
  69. GenerationOptions options,
  70. INamedTypeSymbol generateAsyncOverloadAttributeAttributeSymbol,
  71. INamedTypeSymbol duplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol,
  72. SyntaxReceiver syntaxReceiver)
  73. {
  74. _context = context;
  75. _options = options;
  76. _generateAsyncOverloadAttributeAttributeSymbol = generateAsyncOverloadAttributeAttributeSymbol;
  77. _attributeSymbol = duplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol;
  78. _syntaxReceiver = syntaxReceiver;
  79. }
  80. internal void BuildDuplicatesIfRequired()
  81. {
  82. if (!_syntaxReceiver.CandidateGenerateDeprecatedDuplicatesAttributes.Any(a =>
  83. {
  84. var sm = _context.Compilation.GetSemanticModel(a.SyntaxTree);
  85. var am = sm.GetSymbolInfo(a.Name).Symbol?.ContainingType;
  86. return SymbolEqualityComparer.Default.Equals(am, _attributeSymbol);
  87. }))
  88. {
  89. // The assembly does not have the attribute, so we mustn't run.
  90. return;
  91. }
  92. foreach (var classDeclaration in _syntaxReceiver.CandidateAsyncEnumerableClasses)
  93. {
  94. var sm = _context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree);
  95. var classSymbol = sm.GetDeclaredSymbol(classDeclaration);
  96. if (classSymbol == null || classSymbol.DeclaredAccessibility != Accessibility.Public)
  97. {
  98. continue;
  99. }
  100. // We aren't a general purpose generator, so we only handle the case where the class is nested in a namespace.
  101. if (classDeclaration.Parent is not NamespaceDeclarationSyntax nsDecl)
  102. {
  103. _context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(
  104. "IXNETG001",
  105. "AsyncEnumerable class must be in a namespace declaration",
  106. "AsyncEnumerable class must be declared within a namespace to generate deprecated duplicates",
  107. "Usage",
  108. DiagnosticSeverity.Warning,
  109. isEnabledByDefault: true),
  110. classDeclaration.Identifier.GetLocation()));
  111. continue;
  112. }
  113. if (nsDecl.Parent is not CompilationUnitSyntax file)
  114. { _context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(
  115. "IXNETG002",
  116. "Namespace must be in compilation unit",
  117. "Namespace containing AsyncEnumerable class must be declared within a compilation unit to generate deprecated duplicates",
  118. "Usage",
  119. DiagnosticSeverity.Warning,
  120. isEnabledByDefault: true),
  121. nsDecl.Name.GetLocation()));
  122. continue;
  123. }
  124. var facadeClass = SyntaxFactory.ClassDeclaration("AsyncEnumerableDeprecated")
  125. .WithModifiers(SyntaxFactory.TokenList(
  126. SyntaxFactory.Token(SyntaxKind.PublicKeyword),
  127. SyntaxFactory.Token(SyntaxKind.StaticKeyword),
  128. SyntaxFactory.Token(SyntaxKind.PartialKeyword)));
  129. bool atLeastOneMethod = false;
  130. foreach (var method in classDeclaration.Members.OfType<MethodDeclarationSyntax>())
  131. {
  132. var facadeMethod = method;
  133. string? methodName = null;
  134. if (facadeMethod.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)))
  135. {
  136. // We're only generating duplicates for obsolete methods. (The non-obsolete
  137. // methods have been removed from AsyncEnumerable's public API entirely,
  138. // because either they are now available in .NET 10's System.Linq.AsyncEnumerable
  139. // or we've moved them to System.Interactive.Async.)
  140. if (!facadeMethod.AttributeLists.SelectMany(a => a.Attributes)
  141. .Any(a =>
  142. {
  143. var asym = sm.GetSymbolInfo(a);
  144. return asym.Symbol?.ContainingType.Name == "ObsoleteAttribute" && asym.Symbol.ContainingNamespace?.Name == "System";
  145. }))
  146. {
  147. continue;
  148. }
  149. methodName = facadeMethod.Identifier.Text;
  150. }
  151. else
  152. {
  153. // We also need to emit the facades corresponding to the public methods generated with [GenerateAsyncOverload].
  154. // (Since this code runs as part of the same generator that expands those, we don't get to see the expanded versions
  155. // as input, so we end up slightly duplicating a little of the logic here.)
  156. if (facadeMethod.AttributeLists.SelectMany(a => a.Attributes).Any(a =>
  157. SymbolEqualityComparer.Default.Equals(sm.GetSymbolInfo(a.Name).Symbol?.ContainingType, _generateAsyncOverloadAttributeAttributeSymbol)))
  158. {
  159. var originalMethodSymbol = sm.GetDeclaredSymbol(method)!;
  160. methodName = AsyncOverloadsGenerator.GetMethodNameForGeneratedAsyncMethod(originalMethodSymbol, _options);
  161. facadeMethod = facadeMethod.WithIdentifier(SyntaxFactory.Identifier(methodName));
  162. }
  163. }
  164. if (methodName is not null)
  165. {
  166. // Strip off 'this' from first parameter if it's an extension method.
  167. if (facadeMethod.ParameterList.Parameters.Count > 0)
  168. {
  169. var firstParam = facadeMethod.ParameterList.Parameters[0];
  170. if (firstParam.Modifiers.Any(SyntaxKind.ThisKeyword))
  171. {
  172. var newFirstParam = firstParam.WithModifiers(SyntaxFactory.TokenList());
  173. var newParamList = facadeMethod.ParameterList.WithParameters(
  174. facadeMethod.ParameterList.Parameters.Replace(firstParam, newFirstParam));
  175. facadeMethod = facadeMethod.WithParameterList(newParamList);
  176. }
  177. }
  178. // We don't want to duplicate the implementation. We just generate a simple
  179. // facade that defers to the original method.
  180. var invokeOriginalMethod = SyntaxFactory.InvocationExpression(
  181. SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("AsyncEnumerable"), SyntaxFactory.IdentifierName(methodName)),
  182. SyntaxFactory.ArgumentList(
  183. SyntaxFactory.SeparatedList<ArgumentSyntax>(
  184. facadeMethod.ParameterList.Parameters.Select(p =>
  185. SyntaxFactory.Argument(SyntaxFactory.IdentifierName(p.Identifier))))));
  186. facadeMethod = facadeMethod
  187. .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
  188. .WithAttributeLists(SyntaxFactory.List<AttributeListSyntax>())
  189. .WithBody(null)
  190. .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(invokeOriginalMethod))
  191. .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
  192. .NormalizeWhitespace();
  193. facadeClass = facadeClass.AddMembers(facadeMethod);
  194. atLeastOneMethod = true;
  195. }
  196. }
  197. if (atLeastOneMethod)
  198. {
  199. var namespaceWithClassDup = nsDecl.WithMembers(SyntaxFactory.SingletonList<MemberDeclarationSyntax>(facadeClass));
  200. var fileWithDup = file
  201. .WithLeadingTrivia(file.GetLeadingTrivia().Add(SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.EnableKeyword), true))))
  202. .WithMembers(SyntaxFactory.SingletonList<MemberDeclarationSyntax>(namespaceWithClassDup));
  203. var source = fileWithDup
  204. .NormalizeWhitespace()
  205. .ToFullString();
  206. if (!string.IsNullOrEmpty(source))
  207. {
  208. string existingFileName = System.IO.Path.GetFileNameWithoutExtension(classDeclaration.SyntaxTree.FilePath);
  209. _context.AddSource($"{existingFileName}.DeprecatedDuplicates.g.cs", source);
  210. }
  211. }
  212. }
  213. }
  214. }
  215. }