| File: src\Generators\Shared\GeneratorUtilities.cs | Web Access |
| Project: src\src\Generators\Microsoft.Gen.BuildMetadata\Microsoft.Gen.BuildMetadata.csproj (Microsoft.Gen.BuildMetadata) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; [assembly: System.Resources.NeutralResourcesLanguage("en-us")] #pragma warning disable CA1716 namespace Microsoft.Gen.Shared; #pragma warning restore CA1716 #if !SHARED_PROJECT [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] #endif internal static class GeneratorUtilities { private const string CompilationOutputPath = "build_property.outputpath"; private const string CurrentProjectPath = "build_property.projectdir"; public static string AssemblyName { get; } = typeof(GeneratorUtilities).Assembly.GetName().Name; public static string CurrentVersion { get; } = typeof(GeneratorUtilities).Assembly.GetName().Version.ToString(); public static string GeneratedCodeAttribute { get; } = $"global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{AssemblyName}\", \"{CurrentVersion}\")"; public static string FilePreamble { get; } = @$" // <auto-generated/> #nullable enable "; [ExcludeFromCodeCoverage] public static void Initialize( IncrementalGeneratorInitializationContext context, HashSet<string> fullyQualifiedAttributeNames, Action<Compilation, IEnumerable<SyntaxNode>, SourceProductionContext> process) => Initialize(context, fullyQualifiedAttributeNames, x => x, process); [ExcludeFromCodeCoverage] public static void Initialize( IncrementalGeneratorInitializationContext context, HashSet<string> fullyQualifiedAttributeNames, Func<SyntaxNode, SyntaxNode?> transform, Action<Compilation, IEnumerable<SyntaxNode>, SourceProductionContext> process) { // strip the namespace prefix and the Attribute suffix var shortAttributeNames = new HashSet<string>(); foreach (var n in fullyQualifiedAttributeNames) { var index = n.LastIndexOf('.') + 1; _ = shortAttributeNames.Add(n.Substring(index, n.Length - index - "Attribute".Length)); } var declarations = context.SyntaxProvider .CreateSyntaxProvider( (node, _) => Predicate(node, shortAttributeNames), (gsc, ct) => Filter(gsc, fullyQualifiedAttributeNames, transform, ct)) .Where(t => t is not null) .Select((t, _) => t!); var compilationAndTypes = context.CompilationProvider.Combine(declarations.Collect()); context.RegisterSourceOutput(compilationAndTypes, (spc, source) => { var compilation = source.Left; var nodes = source.Right; if (nodes.IsDefaultOrEmpty) { // nothing to do yet return; } process(compilation, nodes.Distinct(), spc); }); static bool Predicate(SyntaxNode node, HashSet<string> shortAttributeNames) { if (node.IsKind(SyntaxKind.Attribute)) { var attr = (AttributeSyntax)node; // see if we can trivially reject this node and avoid further work if (attr.Name is IdentifierNameSyntax id) { return shortAttributeNames.Contains(id.Identifier.Text); } // too complicated to check further, the filter will have to decide return true; } return false; } static SyntaxNode? Filter(GeneratorSyntaxContext context, HashSet<string> fullyQualifiedAttributeNames, Func<SyntaxNode, SyntaxNode?> transform, CancellationToken cancellationToken) { var attributeSyntax = (AttributeSyntax)context.Node; var ctor = context.SemanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).Symbol as IMethodSymbol; var attributeType = ctor?.ContainingType; if (attributeType != null && fullyQualifiedAttributeNames.Contains(GetAttributeDisplayName(attributeType))) { var node = attributeSyntax.Parent?.Parent; if (node != null) { return transform(node); } } return null; } static string GetAttributeDisplayName(INamedTypeSymbol attributeType) => attributeType.IsGenericType ? attributeType.OriginalDefinition.ToDisplayString() : attributeType.ToDisplayString(); } /// <summary> /// Reports will not be generated during design time to prevent file being written on every keystroke in VS. /// References: /// 1. <see href= /// "https://github.com/dotnet/project-system/blob/c872b4d46e3f308d4b859e684896e1122bdf03c2/docs/design-time-builds.md#determining-whether-a-target-is-running-in-a-design-time-build"> /// Design-time build</see>. /// 2. <see href="https://github.com/dotnet/roslyn/blob/6c9697c56fe39d2335b61ae7c6b342e7b76779ef/docs/features/code-generation.cookbook.md#consume-msbuild-properties-and-metadata"> /// Reading MSBuild Properties in Source Generators</see>. /// </summary> /// <param name="context"><see cref="GeneratorExecutionContext"/>.</param> /// <param name="msBuildProperty">The name of the MSBuild property that determines whether to produce a report.</param> /// <returns>bool value to indicate if reports should be generated.</returns> public static bool ShouldGenerateReport(GeneratorExecutionContext context, string msBuildProperty) { _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(msBuildProperty, out var generateFiles); return string.Equals(generateFiles, bool.TrueString, StringComparison.OrdinalIgnoreCase); } public static bool TryRetrieveOptionsValue(AnalyzerConfigOptions options, string name, out string? value) => options.TryGetValue(name, out value) && !string.IsNullOrWhiteSpace(value); public static string GetDefaultReportOutputPath(AnalyzerConfigOptions options) { if (!TryRetrieveOptionsValue(options, CompilationOutputPath, out var compilationOutputPath)) { return string.Empty; } // If <OutputPath> is absolute - return it right away: if (Path.IsPathRooted(compilationOutputPath)) { return compilationOutputPath!; } // Get <ProjectDir> and combine it with <OutputPath> if the former isn't empty: return TryRetrieveOptionsValue(options, CurrentProjectPath, out var currentProjectPath) ? Path.Combine(currentProjectPath!, compilationOutputPath!) : string.Empty; } }