// 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 System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
namespace CommunityToolkit.Mvvm.SourceGenerators;
///
/// A source generator for a given attribute type.
///
public abstract partial class TransitiveMembersGenerator : ISourceGenerator
{
///
/// The fully qualified name of the attribute type to look for.
///
private readonly string attributeTypeFullName;
///
/// The name of the attribute type to look for.
///
private readonly string attributeTypeName;
///
/// Initializes a new instance of the class.
///
/// The fully qualified name of the attribute type to look for.
protected TransitiveMembersGenerator(string attributeTypeFullName)
{
this.attributeTypeFullName = attributeTypeFullName;
this.attributeTypeName = attributeTypeFullName.Split('.').Last();
}
///
/// Gets a indicating when the generation failed for a given type.
///
protected abstract DiagnosticDescriptor TargetTypeErrorDescriptor { get; }
///
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver(this.attributeTypeFullName));
}
///
public void Execute(GeneratorExecutionContext context)
{
// Get the syntax receiver with the candidate nodes
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
syntaxReceiver.GatheredInfo.Count == 0)
{
return;
}
// Validate the language version
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
{
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
}
// Load the syntax tree with the members to generate
SyntaxTree sourceSyntaxTree = LoadSourceSyntaxTree();
foreach (SyntaxReceiver.Item item in syntaxReceiver.GatheredInfo)
{
if (!ValidateTargetType(context, item.AttributeData, item.ClassDeclaration, item.ClassSymbol, out DiagnosticDescriptor? descriptor))
{
context.ReportDiagnostic(descriptor, item.AttributeSyntax, item.ClassSymbol);
continue;
}
try
{
OnExecute(context, item.AttributeData, item.ClassDeclaration, item.ClassSymbol, sourceSyntaxTree);
}
catch
{
context.ReportDiagnostic(TargetTypeErrorDescriptor, item.AttributeSyntax, item.ClassSymbol);
}
}
}
///
/// Loads the source syntax tree for the current generator.
///
/// The syntax tree with the elements to emit in the generated code.
[Pure]
private SyntaxTree LoadSourceSyntaxTree()
{
string filename = $"CommunityToolkit.Mvvm.SourceGenerators.EmbeddedResources.{this.attributeTypeName.Replace("Attribute", string.Empty)}.cs";
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename);
StreamReader reader = new(stream);
string observableObjectSource = reader.ReadToEnd();
return CSharpSyntaxTree.ParseText(observableObjectSource);
}
///
/// Processes a given target type.
///
/// The input instance to use.
/// The for the current attribute being processed.
/// The node to process.
/// The for .
/// The for the target parsed source.
private void OnExecute(
GeneratorExecutionContext context,
AttributeData attributeData,
ClassDeclarationSyntax classDeclaration,
INamedTypeSymbol classDeclarationSymbol,
SyntaxTree sourceSyntaxTree)
{
ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First();
// Create the class declaration for the user type. This will produce a tree as follows:
//
// :
// {
//
// }
ClassDeclarationSyntax? classDeclarationSyntax =
ClassDeclaration(classDeclaration.Identifier.Text)
.WithModifiers(classDeclaration.Modifiers)
.WithBaseList(sourceDeclaration.BaseList)
.AddMembers(OnLoadDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray());
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
// Add all parent types in ascending order, if any
foreach (TypeDeclarationSyntax? parentType in classDeclaration.Ancestors().OfType())
{
typeDeclarationSyntax = parentType
.WithMembers(SingletonList(typeDeclarationSyntax))
.WithConstraintClauses(List())
.WithBaseList(null)
.WithAttributeLists(List())
.WithoutTrivia();
}
// Create the compilation unit with the namespace and target member.
// From this, we can finally generate the source code to output.
string? namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces));
// Create the final compilation unit to generate (with the full type declaration)
string? source =
CompilationUnit()
.AddMembers(NamespaceDeclaration(IdentifierName(namespaceName))
.WithLeadingTrivia(TriviaList(
Comment("// "),
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
.AddMembers(typeDeclarationSyntax))
.NormalizeWhitespace()
.ToFullString();
// Add the partial type
context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
}
///
/// Loads the nodes to generate from the input parsed tree.
///
/// The input instance to use.
/// The for the current attribute being processed.
/// The node to process.
/// The for .
/// The parsed instance with the source nodes.
/// A sequence of nodes to emit in the generated file.
private IEnumerable OnLoadDeclaredMembers(
GeneratorExecutionContext context,
AttributeData attributeData,
ClassDeclarationSyntax classDeclaration,
INamedTypeSymbol classDeclarationSymbol,
ClassDeclarationSyntax sourceDeclaration)
{
IEnumerable generatedMembers = FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration);
// Add the attributes on each member
return generatedMembers.Select(member =>
{
// [GeneratedCode] is always present
member = member
.WithoutLeadingTrivia()
.AddAttributeLists(AttributeList(SingletonSeparatedList(
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))))
.WithLeadingTrivia(member.GetLeadingTrivia());
// [DebuggerNonUserCode] is not supported over interfaces, events or fields
if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.EventFieldDeclaration and not SyntaxKind.FieldDeclaration)
{
member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))));
}
// [ExcludeFromCodeCoverage] is not supported on interfaces and fields
if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.FieldDeclaration)
{
member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))));
}
// If the target class is sealed, make protected members private and remove the virtual modifier
if (classDeclarationSymbol.IsSealed)
{
return member
.ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword)
.RemoveModifier(SyntaxKind.VirtualKeyword);
}
return member;
});
}
///
/// Validates a target type being processed.
///
/// The input instance to use.
/// The for the current attribute being processed.
/// The node to process.
/// The for .
/// The resulting to emit in case the target type isn't valid.
/// Whether or not the target type is valid and can be processed normally.
protected abstract bool ValidateTargetType(
GeneratorExecutionContext context,
AttributeData attributeData,
ClassDeclarationSyntax classDeclaration,
INamedTypeSymbol classDeclarationSymbol,
[NotNullWhen(false)] out DiagnosticDescriptor? descriptor);
///
/// Filters the nodes to generate from the input parsed tree.
///
/// The input instance to use.
/// The for the current attribute being processed.
/// The node to process.
/// The for .
/// The parsed instance with the source nodes.
/// A sequence of nodes to emit in the generated file.
protected virtual IEnumerable FilterDeclaredMembers(
GeneratorExecutionContext context,
AttributeData attributeData,
ClassDeclarationSyntax classDeclaration,
INamedTypeSymbol classDeclarationSymbol,
ClassDeclarationSyntax sourceDeclaration)
{
return sourceDeclaration.Members;
}
}