// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace System.Linq.Async.SourceGenerator
{
///
/// Builds a facade class containing duplicate definitions of the various deprecated AsyncEnumerable methods
/// as members of AsyncEnumerableDeprecated.
///
///
///
/// This is necessary because of complicated backwards compatibility issues.
///
///
/// The scenario this addresses is when an application has been using System.Linq.Async v6, and was using
/// methods that have been marked as obsolete in System.Linq.Async v7. For example, code might be using
/// the WhereAwaitWithCancellation extension method.
///
///
/// When the developer upgrades such a project to System.Linq.Async v7, we want to ensure that they get
/// deprecation warnings telling them which method they should use instead (e.g., WhereAwaitWithCancellation
/// should be replaced with one of the overloads of Where that .NET 10's System.Linq.AsyncEnumerable
/// defines. However, we don't want to break their build immediately; if they are OK with the deprecation warnings,
/// we want their existing code to continue to work without changes.
///
///
/// This is complicated by the fact that in .NET 10, System.Linq.AsyncEnumerable now defines its own
/// AsyncEnumerable type, meaning that System.Linq.Async's public API must no longer contain a type
/// of that name. (Otherwise, code such as AsyncEnumerable.Range(1, 10) would fail to compile due to the
/// type name being ambiguous.) Thus, the public-facing API of System.Linq.Async in v7 has moved all of the
/// extension methods that it continues to define into a type named AsyncEnumerableDeprecated. (We've
/// called it this because the only methods we need to retain from the public API are the ones that are deprecated.
/// In cases where there are direct replacements in .NET 10's System.Linq.AsyncEnumerable, we've removed
/// the corresponding methods from System.Linq.Async entirely. And in other cases we've moved functionality
/// into System.Interactive.Async. The goal is for everyone to stop using System.Linq.Async, so by
/// definition, if you are still using a method it defines, that method is obsolete.)
///
///
/// But now the problem is that code that continues to use these deprecated methods will expect them to live in
/// AsyncEnumerableDeprecated, because that's where the compiler will find the relevant methods. So
/// the runtime assembly will need to make these methods available in a type of that name. But for binary backwards
/// compatibility, we need every method that our AsyncEnumerable defined in V6 still to be available on
/// a type still called AsyncEnumerable.
///
///
/// In other words, we need to make our API available twice, on two different types. One for code that hasn't been
/// recompiled against v7, and therefore expects all the methods to be in AsyncEnumerable, and one for code
/// that has been recompiled against v7 but which has chosen to continue using deprecated method, and which will
/// expect those to be in AsyncEnumerableDeprecated.
///
///
/// So this generator duplicates public static methods defined by AsyncEnumerable into AsyncEnumerableDeprecated.
/// It also strips off their extension method 'this' modifier from the first parameter, so that they are normal static methods,
/// because otherwise, we get ambiguity errors in our unit tests.
///
///
internal class DeprecatedDuplicateBuilder
{
private readonly GeneratorExecutionContext _context;
private readonly GenerationOptions _options;
private readonly INamedTypeSymbol _generateAsyncOverloadAttributeAttributeSymbol;
private readonly INamedTypeSymbol _attributeSymbol;
private readonly SyntaxReceiver _syntaxReceiver;
public DeprecatedDuplicateBuilder(
GeneratorExecutionContext context,
GenerationOptions options,
INamedTypeSymbol generateAsyncOverloadAttributeAttributeSymbol,
INamedTypeSymbol duplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol,
SyntaxReceiver syntaxReceiver)
{
_context = context;
_options = options;
_generateAsyncOverloadAttributeAttributeSymbol = generateAsyncOverloadAttributeAttributeSymbol;
_attributeSymbol = duplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol;
_syntaxReceiver = syntaxReceiver;
}
internal void BuildDuplicatesIfRequired()
{
if (!_syntaxReceiver.CandidateGenerateDeprecatedDuplicatesAttributes.Any(a =>
{
var sm = _context.Compilation.GetSemanticModel(a.SyntaxTree);
var am = sm.GetSymbolInfo(a.Name).Symbol?.ContainingType;
return SymbolEqualityComparer.Default.Equals(am, _attributeSymbol);
}))
{
// The assembly does not have the attribute, so we mustn't run.
return;
}
foreach (var classDeclaration in _syntaxReceiver.CandidateAsyncEnumerableClasses)
{
var sm = _context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree);
var classSymbol = sm.GetDeclaredSymbol(classDeclaration);
if (classSymbol == null || classSymbol.DeclaredAccessibility != Accessibility.Public)
{
continue;
}
// We aren't a general purpose generator, so we only handle the case where the class is nested in a namespace.
if (classDeclaration.Parent is not NamespaceDeclarationSyntax nsDecl)
{
_context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(
"IXNETG001",
"AsyncEnumerable class must be in a namespace declaration",
"AsyncEnumerable class must be declared within a namespace to generate deprecated duplicates",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true),
classDeclaration.Identifier.GetLocation()));
continue;
}
if (nsDecl.Parent is not CompilationUnitSyntax file)
{ _context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(
"IXNETG002",
"Namespace must be in compilation unit",
"Namespace containing AsyncEnumerable class must be declared within a compilation unit to generate deprecated duplicates",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true),
nsDecl.Name.GetLocation()));
continue;
}
var facadeClass = SyntaxFactory.ClassDeclaration("AsyncEnumerableDeprecated")
.WithModifiers(SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword),
SyntaxFactory.Token(SyntaxKind.PartialKeyword)));
bool atLeastOneMethod = false;
foreach (var method in classDeclaration.Members.OfType())
{
var facadeMethod = method;
string? methodName = null;
if (facadeMethod.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)))
{
// We're only generating duplicates for obsolete methods. (The non-obsolete
// methods have been removed from AsyncEnumerable's public API entirely,
// because either they are now available in .NET 10's System.Linq.AsyncEnumerable
// or we've moved them to System.Interactive.Async.)
if (!facadeMethod.AttributeLists.SelectMany(a => a.Attributes)
.Any(a =>
{
var asym = sm.GetSymbolInfo(a);
return asym.Symbol?.ContainingType.Name == "ObsoleteAttribute" && asym.Symbol.ContainingNamespace?.Name == "System";
}))
{
continue;
}
methodName = facadeMethod.Identifier.Text;
}
else
{
// We also need to emit the facades corresponding to the public methods generated with [GenerateAsyncOverload].
// (Since this code runs as part of the same generator that expands those, we don't get to see the expanded versions
// as input, so we end up slightly duplicating a little of the logic here.)
if (facadeMethod.AttributeLists.SelectMany(a => a.Attributes).Any(a =>
SymbolEqualityComparer.Default.Equals(sm.GetSymbolInfo(a.Name).Symbol?.ContainingType, _generateAsyncOverloadAttributeAttributeSymbol)))
{
var originalMethodSymbol = sm.GetDeclaredSymbol(method)!;
methodName = AsyncOverloadsGenerator.GetMethodNameForGeneratedAsyncMethod(originalMethodSymbol, _options);
facadeMethod = facadeMethod.WithIdentifier(SyntaxFactory.Identifier(methodName));
}
}
if (methodName is not null)
{
// Strip off 'this' from first parameter if it's an extension method.
if (facadeMethod.ParameterList.Parameters.Count > 0)
{
var firstParam = facadeMethod.ParameterList.Parameters[0];
if (firstParam.Modifiers.Any(SyntaxKind.ThisKeyword))
{
var newFirstParam = firstParam.WithModifiers(SyntaxFactory.TokenList());
var newParamList = facadeMethod.ParameterList.WithParameters(
facadeMethod.ParameterList.Parameters.Replace(firstParam, newFirstParam));
facadeMethod = facadeMethod.WithParameterList(newParamList);
}
}
// We don't want to duplicate the implementation. We just generate a simple
// facade that defers to the original method.
var invokeOriginalMethod = SyntaxFactory.InvocationExpression(
SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("AsyncEnumerable"), SyntaxFactory.IdentifierName(methodName)),
SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList(
facadeMethod.ParameterList.Parameters.Select(p =>
SyntaxFactory.Argument(SyntaxFactory.IdentifierName(p.Identifier))))));
facadeMethod = facadeMethod
.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
.WithAttributeLists(SyntaxFactory.List())
.WithBody(null)
.WithExpressionBody(SyntaxFactory.ArrowExpressionClause(invokeOriginalMethod))
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
.NormalizeWhitespace();
facadeClass = facadeClass.AddMembers(facadeMethod);
atLeastOneMethod = true;
}
}
if (atLeastOneMethod)
{
var namespaceWithClassDup = nsDecl.WithMembers(SyntaxFactory.SingletonList(facadeClass));
var fileWithDup = file
.WithLeadingTrivia(file.GetLeadingTrivia().Add(SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.EnableKeyword), true))))
.WithMembers(SyntaxFactory.SingletonList(namespaceWithClassDup));
var source = fileWithDup
.NormalizeWhitespace()
.ToFullString();
if (!string.IsNullOrEmpty(source))
{
string existingFileName = System.IO.Path.GetFileNameWithoutExtension(classDeclaration.SyntaxTree.FilePath);
_context.AddSource($"{existingFileName}.DeprecatedDuplicates.g.cs", source);
}
}
}
}
}
}