using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; namespace PerfectNumbers.Core.Generators; [Generator(LanguageNames.CSharp)] public sealed class DeviceDependentGenerator : IIncrementalGenerator { private const string DeviceDependentTemplateAttributeMetadataName = "PerfectNumbers.Core.DeviceDependentTemplateAttribute"; private const string EnumDependentTemplateAttributeMetadataName = "PerfectNumbers.Core.EnumDependentTemplateAttribute"; private const string NameSuffixAttributeMetadataName = "PerfectNumbers.Core.NameSuffixAttribute"; private static readonly DiagnosticDescriptor TemplateEnumTypeMustBeEnum = new( id: "DDG001", title: "Template attribute requires an enum type", messageFormat: "Template attribute '{0}' requires an enum type, but '{1}' is not an enum.", category: "DeviceDependentGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); private static readonly DiagnosticDescriptor TemplateMustBeTopLevel = new( id: "DDG002", title: "DeviceDependentTemplate must be top-level", messageFormat: "DeviceDependentTemplate can only be used on a top-level class/struct (not nested types).", category: "DeviceDependentGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); private static readonly DiagnosticDescriptor TemplateNotFoundInDeviceParse = new( id: "DDG003", title: "Template not found after preprocessing", messageFormat: "Could not find template type '{0}' after preprocessing for combination '{1}'.", category: "DeviceDependentGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); private static readonly DiagnosticDescriptor DuplicateSingleUseAttribute = new( id: "DDG004", title: "Template attribute used multiple times", messageFormat: "Attribute '{0}' can be applied only once to '{1}'.", category: "DeviceDependentGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); public void Initialize(IncrementalGeneratorInitializationContext context) { IncrementalValuesProvider templates = context.SyntaxProvider.CreateSyntaxProvider( predicate: static (node, _) => node is TypeDeclarationSyntax typeDecl && typeDecl.AttributeLists.Count > 0, transform: static (syntaxContext, cancellationToken) => TryGetTemplateInfo(syntaxContext, cancellationToken)) .Where(static info => info is not null)! .Select(static (info, _) => info!); IncrementalValueProvider options = context.AnalyzerConfigOptionsProvider.Select( static (provider, _) => new GeneratorOptions(provider.GlobalOptions)); IncrementalValueProvider<(Compilation Compilation, GeneratorOptions Options)> compilationAndOptions = context.CompilationProvider.Combine(options).Select(static (pair, _) => (pair.Left, pair.Right)); context.RegisterSourceOutput(templates.Combine(compilationAndOptions), static (productionContext, pair) => { GenerateForTemplate(productionContext, pair.Left, pair.Right.Compilation, pair.Right.Options); }); } private static TemplateInfo? TryGetTemplateInfo(GeneratorSyntaxContext context, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (context.Node is not TypeDeclarationSyntax typeSyntax) { return null; } if (context.SemanticModel.GetDeclaredSymbol(typeSyntax, cancellationToken) is not INamedTypeSymbol templateSymbol) { return null; } return CollectTemplateInfo(templateSymbol, typeSyntax, cancellationToken); } private static void GenerateForTemplate(SourceProductionContext context, TemplateInfo template, Compilation compilation, GeneratorOptions options) { foreach (Diagnostic diagnostic in template.Diagnostics) { context.ReportDiagnostic(diagnostic); } if (!template.Valid || !template.HasAnyTemplateAttribute) { return; } if (template.DeclaringSyntaxReference?.GetSyntax(context.CancellationToken) is not TypeDeclarationSyntax templateSyntax) { return; } if (!IsTopLevel(templateSyntax)) { context.ReportDiagnostic(Diagnostic.Create(TemplateMustBeTopLevel, templateSyntax.Identifier.GetLocation())); return; } string templateName = template.Name; string baseName = templateName.EndsWith("Template", StringComparison.Ordinal) ? templateName.Substring(0, templateName.Length - "Template".Length) : templateName; SourceText fileText = templateSyntax.SyntaxTree.GetText(context.CancellationToken); CSharpParseOptions baseParseOptions = (CSharpParseOptions)templateSyntax.SyntaxTree.Options; string filePath = templateSyntax.SyntaxTree.FilePath; foreach (TemplateVariant variant in template.ExpandVariants()) { CSharpParseOptions parseOptions = WithDefines(baseParseOptions, variant.PreprocessorSymbols); SyntaxTree deviceTree = CSharpSyntaxTree.ParseText( fileText.ToString(), parseOptions, filePath, encoding: Encoding.UTF8); CompilationUnitSyntax deviceRoot = deviceTree.GetCompilationUnitRoot(context.CancellationToken); TypeDeclarationSyntax? deviceTemplateSyntax = FindTemplateDeclaration(deviceRoot, templateName); if (deviceTemplateSyntax is null) { context.ReportDiagnostic(Diagnostic.Create( TemplateNotFoundInDeviceParse, templateSyntax.Identifier.GetLocation(), templateName, variant.CombinationDisplay)); continue; } ReportDeviceSpecificTemplateErrors(context, deviceTree); string generatedTypeName = baseName + variant.NameSuffix + template.NameSuffix; TypeDeclarationSyntax generatedTypeSyntax = TransformTemplateType(deviceTemplateSyntax, generatedTypeName); CompilationUnitSyntax generatedRoot = BuildGeneratedFile( externs: deviceRoot.Externs, usings: deviceRoot.Usings, containingNamespace: template.ContainingNamespace, typeDeclaration: generatedTypeSyntax); generatedRoot = (CompilationUnitSyntax)new ConditionalDirectiveAndDisabledTextStripper().Visit(generatedRoot)!; generatedRoot = generatedRoot.NormalizeWhitespace(); string generatedFileName = baseName + "." + variant.FileSuffix + template.NameSuffix + ".generated.cs"; string hintName = options.BuildHintName(template.ContainingNamespace, generatedFileName); context.AddSource( hintName, SourceText.From("// \n#nullable enable\n" + generatedRoot.ToFullString(), Encoding.UTF8)); } } private static void ReportDeviceSpecificTemplateErrors(SourceProductionContext context, SyntaxTree deviceTree) { foreach (Diagnostic diagnostic in deviceTree.GetDiagnostics(context.CancellationToken)) { if (diagnostic.Severity == DiagnosticSeverity.Error) { context.ReportDiagnostic(diagnostic); } } } private static bool IsTopLevel(TypeDeclarationSyntax typeDeclaration) => typeDeclaration.Parent is NamespaceDeclarationSyntax || typeDeclaration.Parent is FileScopedNamespaceDeclarationSyntax || typeDeclaration.Parent is CompilationUnitSyntax; private static CSharpParseOptions WithDefines(CSharpParseOptions baseParseOptions, IEnumerable defines) => baseParseOptions.WithPreprocessorSymbols(baseParseOptions.PreprocessorSymbolNames.Concat(defines).Distinct()); private static TypeDeclarationSyntax? FindTemplateDeclaration(CompilationUnitSyntax root, string templateName) { foreach (TypeDeclarationSyntax typeDecl in root.DescendantNodes().OfType()) { if (string.Equals(typeDecl.Identifier.ValueText, templateName, StringComparison.Ordinal)) { return typeDecl; } } return null; } private static TypeDeclarationSyntax TransformTemplateType(TypeDeclarationSyntax templateType, string generatedTypeName) { TypeDeclarationSyntax withoutAttribute = RemoveTemplateAttributes(templateType); TypeDeclarationSyntax renamed = (TypeDeclarationSyntax)new SelfIdentifierRewriter(templateType.Identifier.ValueText, generatedTypeName).Visit(withoutAttribute)!; return renamed.WithIdentifier(SyntaxFactory.Identifier(generatedTypeName)); } private static TypeDeclarationSyntax RemoveTemplateAttributes(TypeDeclarationSyntax typeDeclaration) { if (typeDeclaration.AttributeLists.Count == 0) { return typeDeclaration; } var newLists = new List(typeDeclaration.AttributeLists.Count); foreach (AttributeListSyntax list in typeDeclaration.AttributeLists) { SeparatedSyntaxList attributes = list.Attributes; var newAttributes = new List(attributes.Count); foreach (AttributeSyntax attribute in attributes) { if (IsTemplateAttribute(attribute.Name)) { continue; } newAttributes.Add(attribute); } if (newAttributes.Count == 0) { continue; } newLists.Add(list.WithAttributes(SyntaxFactory.SeparatedList(newAttributes))); } return typeDeclaration.WithAttributeLists(SyntaxFactory.List(newLists)); } private sealed class SelfIdentifierRewriter : CSharpSyntaxRewriter { private readonly string templateName; private readonly string generatedName; public SelfIdentifierRewriter(string templateName, string generatedName) { this.templateName = templateName; this.generatedName = generatedName; } public override SyntaxToken VisitToken(SyntaxToken token) { if (token.IsKind(SyntaxKind.IdentifierToken) && string.Equals(token.ValueText, templateName, StringComparison.Ordinal)) { return SyntaxFactory.Identifier(token.LeadingTrivia, generatedName, token.TrailingTrivia); } return base.VisitToken(token); } } private static bool IsTemplateAttribute(NameSyntax attributeName) { string text = attributeName.ToString(); return text.EndsWith("DeviceDependentTemplate", StringComparison.Ordinal) || text.EndsWith("DeviceDependentTemplateAttribute", StringComparison.Ordinal) || text.EndsWith("EnumDependentTemplate", StringComparison.Ordinal) || text.EndsWith("EnumDependentTemplateAttribute", StringComparison.Ordinal) || text.EndsWith("NameSuffix", StringComparison.Ordinal) || text.EndsWith("NameSuffixAttribute", StringComparison.Ordinal); } private static CompilationUnitSyntax BuildGeneratedFile( SyntaxList externs, SyntaxList usings, string containingNamespace, TypeDeclarationSyntax typeDeclaration) { MemberDeclarationSyntax member; if (string.IsNullOrEmpty(containingNamespace)) { member = typeDeclaration; } else { member = SyntaxFactory .FileScopedNamespaceDeclaration(SyntaxFactory.ParseName(containingNamespace)) .WithMembers(SyntaxFactory.SingletonList(typeDeclaration)); } return SyntaxFactory .CompilationUnit() .WithExterns(externs) .WithUsings(usings) .WithMembers(SyntaxFactory.SingletonList(member)); } private sealed class TemplateInfo { private TemplateInfo( INamedTypeSymbol templateType, ImmutableArray enumAttributes, DeviceTemplateInfo? deviceAttribute, string nameSuffix, ImmutableArray diagnostics) { TemplateType = templateType; EnumAttributes = enumAttributes; DeviceAttribute = deviceAttribute; NameSuffix = nameSuffix; Diagnostics = diagnostics; } public INamedTypeSymbol TemplateType { get; } public ImmutableArray EnumAttributes { get; } public DeviceTemplateInfo? DeviceAttribute { get; } public string NameSuffix { get; } public ImmutableArray Diagnostics { get; } public bool Valid => Diagnostics.IsDefaultOrEmpty; public bool HasAnyTemplateAttribute => EnumAttributes.Length > 0 || DeviceAttribute is not null; public string Name => TemplateType.Name; public string ContainingNamespace => TemplateType.ContainingNamespace?.IsGlobalNamespace == true ? string.Empty : TemplateType.ContainingNamespace.ToDisplayString(); public SyntaxReference? DeclaringSyntaxReference => TemplateType.DeclaringSyntaxReferences.FirstOrDefault(); public IEnumerable ExpandVariants() { var combinations = ImmutableArray.Create(new TemplateVariantBuilder()); foreach (EnumTemplateInfo enumInfo in EnumAttributes) { combinations = ExpandEnum(combinations, enumInfo); } bool hasEnumAttributes = EnumAttributes.Length > 0; if (DeviceAttribute is not null) { combinations = ExpandDevice(combinations, DeviceAttribute.Value, hasEnumAttributes); } foreach (TemplateVariantBuilder builder in combinations) { yield return new TemplateVariant(builder.Symbols, builder.NameSuffix); } } private static ImmutableArray ExpandEnum( ImmutableArray current, EnumTemplateInfo enumInfo) { var builder = ImmutableArray.CreateBuilder(current.Length * enumInfo.Values.Length); foreach (TemplateVariantBuilder existing in current) { foreach (string value in enumInfo.Values) { builder.Add(existing.Append( symbol: $"{enumInfo.EnumType.Name}_{value}", namePart: $"For{value}{enumInfo.EnumType.Name}")); } } return builder.ToImmutable(); } private static ImmutableArray ExpandDevice( ImmutableArray current, DeviceTemplateInfo deviceInfo, bool prefixWithFor) { var builder = ImmutableArray.CreateBuilder(current.Length * deviceInfo.Values.Length); foreach (TemplateVariantBuilder existing in current) { foreach (string value in deviceInfo.Values) { string namePart = prefixWithFor ? $"For{value}" : value; builder.Add(existing.Append( symbol: "DEVICE_" + value.ToUpperInvariant(), namePart: namePart)); } } return builder.ToImmutable(); } public static TemplateInfo Create( INamedTypeSymbol templateType, ImmutableArray enumAttributes, DeviceTemplateInfo? deviceAttribute, string? nameSuffix, ImmutableArray diagnostics) => new( templateType, enumAttributes, deviceAttribute, nameSuffix ?? string.Empty, diagnostics); } private readonly struct EnumTemplateInfo { public EnumTemplateInfo(INamedTypeSymbol enumType, ImmutableArray values, Location attributeLocation) { EnumType = enumType; Values = values; AttributeLocation = attributeLocation; } public INamedTypeSymbol EnumType { get; } public ImmutableArray Values { get; } public Location AttributeLocation { get; } } private readonly struct DeviceTemplateInfo { public DeviceTemplateInfo(INamedTypeSymbol enumType, ImmutableArray values, Location attributeLocation) { EnumType = enumType; Values = values; AttributeLocation = attributeLocation; } public INamedTypeSymbol EnumType { get; } public ImmutableArray Values { get; } public Location AttributeLocation { get; } } private readonly struct TemplateVariant { public TemplateVariant(ImmutableArray preprocessorSymbols, string nameSuffix) { PreprocessorSymbols = preprocessorSymbols; NameSuffix = nameSuffix; } public ImmutableArray PreprocessorSymbols { get; } public string NameSuffix { get; } public string FileSuffix => NameSuffix; public string CombinationDisplay => string.Join(", ", PreprocessorSymbols); } private sealed class TemplateVariantBuilder { public TemplateVariantBuilder() { Symbols = ImmutableArray.Empty; NameSuffix = string.Empty; } private TemplateVariantBuilder(ImmutableArray symbols, string nameSuffix) { Symbols = symbols; NameSuffix = nameSuffix; } public ImmutableArray Symbols { get; } public string NameSuffix { get; } public TemplateVariantBuilder Append(string symbol, string namePart) { return new TemplateVariantBuilder(Symbols.Add(symbol), NameSuffix + namePart); } } private static TemplateInfo? CollectTemplateInfo(INamedTypeSymbol templateSymbol, TypeDeclarationSyntax typeSyntax, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ImmutableArray attributes = templateSymbol.GetAttributes(); var enumAttributes = new List(); DeviceTemplateInfo? deviceAttribute = null; string? nameSuffix = null; var diagnostics = ImmutableArray.CreateBuilder(); AttributeData? nameSuffixAttribute = null; AttributeData? deviceTemplateAttribute = null; foreach (AttributeData attribute in attributes) { string? metadataName = attribute.AttributeClass?.ToDisplayString(); if (metadataName is null) { continue; } if (string.Equals(metadataName, EnumDependentTemplateAttributeMetadataName, StringComparison.Ordinal)) { enumAttributes.AddRange(CreateEnumTemplateInfo(attribute, TemplateEnumTypeMustBeEnum, diagnostics)); } else if (string.Equals(metadataName, DeviceDependentTemplateAttributeMetadataName, StringComparison.Ordinal)) { if (deviceTemplateAttribute is not null) { diagnostics.Add(Diagnostic.Create( DuplicateSingleUseAttribute, attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken)?.GetLocation() ?? typeSyntax.GetLocation(), DeviceDependentTemplateAttributeMetadataName, templateSymbol.Name)); continue; } deviceTemplateAttribute = attribute; } else if (string.Equals(metadataName, NameSuffixAttributeMetadataName, StringComparison.Ordinal)) { if (nameSuffixAttribute is not null) { diagnostics.Add(Diagnostic.Create( DuplicateSingleUseAttribute, attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken)?.GetLocation() ?? typeSyntax.GetLocation(), NameSuffixAttributeMetadataName, templateSymbol.Name)); continue; } nameSuffixAttribute = attribute; } } if (deviceTemplateAttribute is not null) { var deviceInfo = CreateEnumTemplateInfo(deviceTemplateAttribute, TemplateEnumTypeMustBeEnum, diagnostics).FirstOrDefault(); if (deviceInfo.EnumType is not null) { deviceAttribute = new DeviceTemplateInfo(deviceInfo.EnumType, deviceInfo.Values, deviceInfo.AttributeLocation); } } if (nameSuffixAttribute is not null) { nameSuffix = ExtractSuffix(nameSuffixAttribute); } if (!enumAttributes.Any() && deviceAttribute is null && nameSuffixAttribute is null) { return null; } ImmutableArray orderedEnums = enumAttributes .OrderBy(static e => e.AttributeLocation.SourceSpan.Start) .ToImmutableArray(); return TemplateInfo.Create( templateSymbol, orderedEnums, deviceAttribute, nameSuffix, diagnostics.ToImmutable()); } private static IEnumerable CreateEnumTemplateInfo( AttributeData attribute, DiagnosticDescriptor diagnosticDescriptor, ImmutableArray.Builder diagnostics) { if (attribute.ConstructorArguments.Length < 1) { return Array.Empty(); } if (attribute.ConstructorArguments[0].Value is not INamedTypeSymbol enumTypeSymbol) { return Array.Empty(); } if (enumTypeSymbol.TypeKind != TypeKind.Enum) { Location location = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? Location.None; diagnostics.Add(Diagnostic.Create(diagnosticDescriptor, location, attribute.AttributeClass?.Name ?? "TemplateAttribute", enumTypeSymbol.ToDisplayString())); return Array.Empty(); } ImmutableArray enumValues = enumTypeSymbol .GetMembers() .OfType() .Where(static f => !f.IsImplicitlyDeclared && f.HasConstantValue) .Select(static f => f.Name) .ToImmutableArray(); Location attributeLocation = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? Location.None; return new[] { new EnumTemplateInfo(enumTypeSymbol, enumValues, attributeLocation) }; } private static string? ExtractSuffix(AttributeData attributeData) { if (attributeData.ConstructorArguments.Length > 0) { return attributeData.ConstructorArguments[0].Value as string; } foreach (KeyValuePair argument in attributeData.NamedArguments) { if (string.Equals(argument.Key, "suffix", StringComparison.OrdinalIgnoreCase)) { return argument.Value.Value as string; } } return null; } private readonly struct GeneratorOptions { private const string GeneratedDirectoryName = "generated"; private const string GeneratedRootOptionName = "build_property.DeviceDependentGenerator_OutputRoot"; private readonly AnalyzerConfigOptions globalOptions; public GeneratorOptions(AnalyzerConfigOptions globalOptions) { this.globalOptions = globalOptions; } public string BuildHintName(string containingNamespace, string generatedFileName) { string root = GetGeneratedRoot(); string trimmedRoot = string.IsNullOrWhiteSpace(root) ? GeneratedDirectoryName : root.Replace('\\', '/').Trim('/').Trim(); if (string.IsNullOrWhiteSpace(trimmedRoot)) { trimmedRoot = GeneratedDirectoryName; } string namespacePath = string.IsNullOrWhiteSpace(containingNamespace) ? string.Empty : containingNamespace.Replace('.', '/').Trim('/'); if (namespacePath.Length == 0) { return trimmedRoot + "/" + generatedFileName; } return trimmedRoot + "/" + namespacePath + "/" + generatedFileName; } private string GetGeneratedRoot() { if (globalOptions.TryGetValue(GeneratedRootOptionName, out string? configuredRoot) && !string.IsNullOrWhiteSpace(configuredRoot)) { return configuredRoot; } return GeneratedDirectoryName; } } private sealed class ConditionalDirectiveAndDisabledTextStripper : CSharpSyntaxRewriter { public override SyntaxToken VisitToken(SyntaxToken token) => token.WithLeadingTrivia(Filter(token.LeadingTrivia)).WithTrailingTrivia(Filter(token.TrailingTrivia)); private static SyntaxTriviaList Filter(SyntaxTriviaList list) { var builder = ImmutableArray.CreateBuilder(list.Count); foreach (SyntaxTrivia trivia in list) { SyntaxKind kind = trivia.Kind(); if (kind is SyntaxKind.DisabledTextTrivia or SyntaxKind.IfDirectiveTrivia or SyntaxKind.ElifDirectiveTrivia or SyntaxKind.ElseDirectiveTrivia or SyntaxKind.EndIfDirectiveTrivia) { continue; } builder.Add(trivia); } return SyntaxFactory.TriviaList(builder.ToImmutable()); } } }