diff --git a/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs b/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs
index 9f02466..d7a1fd7 100644
--- a/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs
+++ b/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs
@@ -34,7 +34,8 @@ public override void Initialize(AnalysisContext context)
packageId == "SponsorableLib";
}).Select(x => File.GetLastWriteTime(x.Path)).OrderByDescending(x => x).FirstOrDefault();
- var status = Diagnostics.GetStatus(Funding.Product);
+ var status = Diagnostics.GetOrSetStatus(() => c.Options);
+
if (installed != default)
Tracing.Trace($"Status: {status}, Installed: {(DateTime.Now - installed).Humanize()} ago");
else
diff --git a/src/SponsorLink/Analyzer/StatusReportingGenerator.cs b/src/SponsorLink/Analyzer/StatusReportingGenerator.cs
new file mode 100644
index 0000000..0a13b1c
--- /dev/null
+++ b/src/SponsorLink/Analyzer/StatusReportingGenerator.cs
@@ -0,0 +1,20 @@
+using Devlooped.Sponsors;
+using Microsoft.CodeAnalysis;
+using static Devlooped.Sponsors.SponsorLink;
+
+namespace Analyzer;
+
+[Generator]
+public class StatusReportingGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ context.RegisterSourceOutput(
+ context.GetSponsorManifests(),
+ (spc, source) =>
+ {
+ var status = Diagnostics.GetOrSetStatus(source);
+ spc.AddSource("StatusReporting.cs", $"// Status: {status}");
+ });
+ }
+}
diff --git a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs b/src/SponsorLink/SponsorLink/DiagnosticsManager.cs
index c22ecc8..1d31b39 100644
--- a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs
+++ b/src/SponsorLink/SponsorLink/DiagnosticsManager.cs
@@ -2,9 +2,16 @@
#nullable enable
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
+using System.IO;
+using System.Linq;
using Humanizer;
+using Humanizer.Localisation;
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static Devlooped.Sponsors.SponsorLink;
namespace Devlooped.Sponsors;
@@ -14,41 +21,22 @@ namespace Devlooped.Sponsors;
///
class DiagnosticsManager
{
- ///
- /// Acceses the diagnostics dictionary for the current .
- ///
- ConcurrentDictionary Diagnostics
- => AppDomainDictionary.Get>(nameof(Diagnostics));
-
- ///
- /// Creates a descriptor from well-known diagnostic kinds.
- ///
- /// The names of the sponsorable accounts that can be funded for the given product.
- /// The product or project developed by the sponsorable(s).
- /// Custom prefix to use for diagnostic IDs.
- /// The kind of status diagnostic to create.
- /// The given .
- /// The is not one of the known ones.
- public DiagnosticDescriptor GetDescriptor(string[] sponsorable, string product, string prefix, SponsorStatus status) => status switch
+ public static Dictionary KnownDescriptors { get; } = new()
{
- SponsorStatus.Unknown => CreateUnknown(sponsorable, product, prefix),
- SponsorStatus.Sponsor => CreateSponsor(sponsorable, prefix),
- SponsorStatus.Expiring => CreateExpiring(sponsorable, prefix),
- SponsorStatus.Expired => CreateExpired(sponsorable, prefix),
- _ => throw new NotImplementedException(),
+ // Requires:
+ //
+ //
+ { SponsorStatus.Unknown, CreateUnknown([.. Sponsorables.Keys], Funding.Product, Funding.Prefix) },
+ { SponsorStatus.Sponsor, CreateSponsor([.. Sponsorables.Keys], Funding.Prefix) },
+ { SponsorStatus.Expiring, CreateExpiring([.. Sponsorables.Keys], Funding.Prefix) },
+ { SponsorStatus.Expired, CreateExpired([.. Sponsorables.Keys], Funding.Prefix) },
};
///
- /// Pushes a diagnostic for the given product. If an existing one exists, it is replaced.
+ /// Acceses the diagnostics dictionary for the current .
///
- /// The same diagnostic that was pushed, for chained invocations.
- public Diagnostic Push(string product, Diagnostic diagnostic)
- {
- // Directly sets, since we only expect to get one warning per sponsorable+product
- // combination.
- Diagnostics[product] = diagnostic;
- return diagnostic;
- }
+ ConcurrentDictionary Diagnostics
+ => AppDomainDictionary.Get>(nameof(Diagnostics));
///
/// Attemps to remove a diagnostic for the given product.
@@ -62,17 +50,19 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
}
///
- /// Gets the status of the given product based on a previously stored diagnostic.
+ /// Gets the status of the given product based on a previously stored diagnostic.
+ /// To ensure the value is always set before returning, use .
+ /// This method is safe to use (and would get a non-null value) in analyzers that run after CompilationStartAction(see
+ /// https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md under Ordering of actions).
///
- /// The product to check status for.
/// Optional that was reported, if any.
- public SponsorStatus? GetStatus(string product)
+ public SponsorStatus? GetStatus()
{
// NOTE: the SponsorLinkAnalyzer.SetStatus uses diagnostic properties to store the
// kind of diagnostic as a simple string instead of the enum. We do this so that
// multiple analyzers or versions even across multiple products, which all would
// have their own enum, can still share the same diagnostic kind.
- if (Diagnostics.TryGetValue(product, out var diagnostic) &&
+ if (Diagnostics.TryGetValue(Funding.Product, out var diagnostic) &&
diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value))
{
// Switch on value matching DiagnosticKind names
@@ -89,7 +79,76 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
return null;
}
- static DiagnosticDescriptor CreateSponsor(string[] sponsorable, string prefix) => new(
+ ///
+ /// Gets the status of the , or sets it from
+ /// the given set of if not already set.
+ ///
+ public SponsorStatus GetOrSetStatus(ImmutableArray manifests)
+ => GetOrSetStatus(() => manifests);
+
+ ///
+ /// Gets the status of the , or sets it from
+ /// the given analyzer if not already set.
+ ///
+ public SponsorStatus GetOrSetStatus(Func options)
+ => GetOrSetStatus(() => options().GetSponsorManifests());
+
+ SponsorStatus GetOrSetStatus(Func> getManifests)
+ {
+ if (GetStatus() is { } status)
+ return status;
+
+ if (!SponsorLink.TryRead(out var claims, getManifests().Select(text =>
+ (text.GetText()?.ToString() ?? "", Sponsorables[Path.GetFileNameWithoutExtension(text.Path)]))) ||
+ claims.GetExpiration() is not DateTime exp)
+ {
+ // report unknown, either unparsed manifest or one with no expiration (which we never emit).
+ Push(Funding.Product, Diagnostic.Create(KnownDescriptors[SponsorStatus.Unknown], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Unknown)),
+ Funding.Product, Sponsorables.Keys.Humanize(Resources.Or)));
+ return SponsorStatus.Unknown;
+ }
+ else if (exp < DateTime.Now)
+ {
+ // report expired or expiring soon if still within the configured days of grace period
+ if (exp.AddDays(Funding.Grace) < DateTime.Now)
+ {
+ // report expiring soon
+ Push(Funding.Product, Diagnostic.Create(KnownDescriptors[SponsorStatus.Expiring], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expiring))));
+ return SponsorStatus.Expiring;
+ }
+ else
+ {
+ // report expired
+ Push(Funding.Product, Diagnostic.Create(KnownDescriptors[SponsorStatus.Expired], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expired))));
+ return SponsorStatus.Expired;
+ }
+ }
+ else
+ {
+ // report sponsor
+ Push(Funding.Product, Diagnostic.Create(KnownDescriptors[SponsorStatus.Sponsor], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Sponsor)),
+ Funding.Product));
+ return SponsorStatus.Sponsor;
+ }
+ }
+
+ ///
+ /// Pushes a diagnostic for the given product.
+ ///
+ /// The same diagnostic that was pushed, for chained invocations.
+ Diagnostic Push(string product, Diagnostic diagnostic)
+ {
+ // We only expect to get one warning per sponsorable+product
+ // combination, and first one to set wins.
+ Diagnostics.TryAdd(product, diagnostic);
+ return diagnostic;
+ }
+
+ internal static DiagnosticDescriptor CreateSponsor(string[] sponsorable, string prefix) => new(
$"{prefix}100",
Resources.Sponsor_Title,
Resources.Sponsor_Message,
@@ -100,7 +159,7 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
helpLinkUri: "https://github.com/devlooped#sponsorlink",
"DoesNotSupportF1Help");
- static DiagnosticDescriptor CreateUnknown(string[] sponsorable, string product, string prefix) => new(
+ internal static DiagnosticDescriptor CreateUnknown(string[] sponsorable, string product, string prefix) => new(
$"{prefix}101",
Resources.Unknown_Title,
Resources.Unknown_Message,
@@ -113,7 +172,7 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
helpLinkUri: "https://github.com/devlooped#sponsorlink",
WellKnownDiagnosticTags.NotConfigurable);
- static DiagnosticDescriptor CreateExpiring(string[] sponsorable, string prefix) => new(
+ internal static DiagnosticDescriptor CreateExpiring(string[] sponsorable, string prefix) => new(
$"{prefix}103",
Resources.Expiring_Title,
Resources.Expiring_Message,
@@ -124,7 +183,7 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
helpLinkUri: "https://github.com/devlooped#autosync",
"DoesNotSupportF1Help", WellKnownDiagnosticTags.NotConfigurable);
- static DiagnosticDescriptor CreateExpired(string[] sponsorable, string prefix) => new(
+ internal static DiagnosticDescriptor CreateExpired(string[] sponsorable, string prefix) => new(
$"{prefix}104",
Resources.Expired_Title,
Resources.Expired_Message,
diff --git a/src/SponsorLink/SponsorLink/SponsorLink.cs b/src/SponsorLink/SponsorLink/SponsorLink.cs
index a4c0675..b3b1cf3 100644
--- a/src/SponsorLink/SponsorLink/SponsorLink.cs
+++ b/src/SponsorLink/SponsorLink/SponsorLink.cs
@@ -2,10 +2,14 @@
#nullable enable
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Claims;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
@@ -59,6 +63,32 @@ static partial class SponsorLink
.Select(DateTimeOffset.FromUnixTimeSeconds)
.Max().DateTime is var exp && exp == DateTime.MinValue ? null : exp;
+ ///
+ /// Gets all sponsor manifests from the provided analyzer options.
+ ///
+ public static ImmutableArray GetSponsorManifests(this AnalyzerOptions? options)
+ => options == null ? ImmutableArray.Create() : options.AdditionalFiles
+ .Where(x =>
+ options.AnalyzerConfigOptionsProvider.GetOptions(x).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) &&
+ itemType == "SponsorManifest" &&
+ Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(x.Path)))
+ .ToImmutableArray();
+
+ ///
+ /// Gets all sponsor manifests from the provided analyzer options.
+ ///
+ public static IncrementalValueProvider> GetSponsorManifests(this IncrementalGeneratorInitializationContext context)
+ => context.AdditionalTextsProvider.Combine(context.AnalyzerConfigOptionsProvider)
+ .Where(source =>
+ {
+ var (text, options) = source;
+ return options.GetOptions(text).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) &&
+ itemType == "SponsorManifest" &&
+ Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(text.Path));
+ })
+ .Select((source, c) => source.Left)
+ .Collect();
+
///
/// Reads all manifests, validating their signatures.
///
diff --git a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs
index d87c238..dc2a9d3 100644
--- a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs
+++ b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs
@@ -1,13 +1,9 @@
//
#nullable enable
using System;
-using System.Collections.Generic;
using System.Collections.Immutable;
-using System.Diagnostics;
using System.IO;
using System.Linq;
-using Humanizer;
-using Humanizer.Localisation;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using static Devlooped.Sponsors.SponsorLink;
@@ -20,18 +16,7 @@ namespace Devlooped.Sponsors;
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class SponsorLinkAnalyzer : DiagnosticAnalyzer
{
- static readonly Dictionary descriptors = new()
- {
- // Requires:
- //
- //
- { SponsorStatus.Unknown, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Unknown) },
- { SponsorStatus.Sponsor, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Sponsor) },
- { SponsorStatus.Expiring, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Expiring) },
- { SponsorStatus.Expired, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Expired) },
- };
-
- public override ImmutableArray SupportedDiagnostics { get; } = descriptors.Values.ToImmutableArray();
+ public override ImmutableArray SupportedDiagnostics { get; } = DiagnosticsManager.KnownDescriptors.Values.ToImmutableArray();
#pragma warning disable RS1026 // Enable concurrent execution
public override void Initialize(AnalysisContext context)
@@ -49,15 +34,8 @@ public override void Initialize(AnalysisContext context)
// analyzers can report the same diagnostic and we want to avoid duplicates.
context.RegisterCompilationStartAction(ctx =>
{
- var manifests = ctx.Options.AdditionalFiles
- .Where(x =>
- ctx.Options.AnalyzerConfigOptionsProvider.GetOptions(x).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) &&
- itemType == "SponsorManifest" &&
- Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(x.Path)))
- .ToImmutableArray();
-
// Setting the status early allows other analyzers to potentially check for it.
- var status = SetStatus(manifests);
+ var status = Diagnostics.GetOrSetStatus(() => ctx.Options);
// Never report any diagnostic unless we're in an editor.
if (IsEditor)
@@ -108,44 +86,4 @@ public override void Initialize(AnalysisContext context)
});
#pragma warning restore RS1013 // Start action has no registered non-end actions
}
-
- SponsorStatus SetStatus(ImmutableArray manifests)
- {
- if (!SponsorLink.TryRead(out var claims, manifests.Select(text =>
- (text.GetText()?.ToString() ?? "", Sponsorables[Path.GetFileNameWithoutExtension(text.Path)]))) ||
- claims.GetExpiration() is not DateTime exp)
- {
- // report unknown, either unparsed manifest or one with no expiration (which we never emit).
- Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Unknown], null,
- properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Unknown)),
- Funding.Product, Sponsorables.Keys.Humanize(Resources.Or)));
- return SponsorStatus.Unknown;
- }
- else if (exp < DateTime.Now)
- {
- // report expired or expiring soon if still within the configured days of grace period
- if (exp.AddDays(Funding.Grace) < DateTime.Now)
- {
- // report expiring soon
- Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Expiring], null,
- properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expiring))));
- return SponsorStatus.Expiring;
- }
- else
- {
- // report expired
- Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Expired], null,
- properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expired))));
- return SponsorStatus.Expired;
- }
- }
- else
- {
- // report sponsor
- Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Sponsor], null,
- properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Sponsor)),
- Funding.Product));
- return SponsorStatus.Sponsor;
- }
- }
}
diff --git a/src/SponsorLink/Tests/Sample.cs b/src/SponsorLink/Tests/Sample.cs
index 6249e62..3ea4a32 100644
--- a/src/SponsorLink/Tests/Sample.cs
+++ b/src/SponsorLink/Tests/Sample.cs
@@ -4,6 +4,7 @@
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using Analyzer::Devlooped.Sponsors;
+using Microsoft.CodeAnalysis;
using Xunit;
using Xunit.Abstractions;
@@ -29,7 +30,7 @@ public void Test(string culture, SponsorStatus kind)
Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture =
culture == "" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture);
- var diag = new DiagnosticsManager().GetDescriptor(["foo"], "bar", "FB", kind);
+ var diag = GetDescriptor(["foo"], "bar", "FB", kind);
output.WriteLine(diag.Title.ToString());
output.WriteLine(diag.MessageFormat.ToString());
@@ -56,4 +57,13 @@ public void RenderSponsorables()
});
}
}
+
+ DiagnosticDescriptor GetDescriptor(string[] sponsorable, string product, string prefix, SponsorStatus status) => status switch
+ {
+ SponsorStatus.Unknown => DiagnosticsManager.CreateUnknown(sponsorable, product, prefix),
+ SponsorStatus.Sponsor => DiagnosticsManager.CreateSponsor(sponsorable, prefix),
+ SponsorStatus.Expiring => DiagnosticsManager.CreateExpiring(sponsorable, prefix),
+ SponsorStatus.Expired => DiagnosticsManager.CreateExpired(sponsorable, prefix),
+ _ => throw new NotImplementedException(),
+ };
}
\ No newline at end of file
diff --git a/src/SponsorLink/Tests/Tests.csproj b/src/SponsorLink/Tests/Tests.csproj
index 1f5dd37..5082c97 100644
--- a/src/SponsorLink/Tests/Tests.csproj
+++ b/src/SponsorLink/Tests/Tests.csproj
@@ -2,6 +2,7 @@
net8.0
+ true