22#nullable enable
33using System ;
44using System . Collections . Concurrent ;
5+ using System . Collections . Generic ;
6+ using System . Collections . Immutable ;
57using System . Globalization ;
8+ using System . IO ;
9+ using System . Linq ;
610using Humanizer ;
11+ using Humanizer . Localisation ;
712using Microsoft . CodeAnalysis ;
13+ using Microsoft . CodeAnalysis . Diagnostics ;
14+ using static Devlooped . Sponsors . SponsorLink ;
815
916namespace Devlooped . Sponsors ;
1017
@@ -14,41 +21,22 @@ namespace Devlooped.Sponsors;
1421/// </summary>
1522class DiagnosticsManager
1623{
17- /// <summary>
18- /// Acceses the diagnostics dictionary for the current <see cref="AppDomain"/>.
19- /// </summary>
20- ConcurrentDictionary < string , Diagnostic > Diagnostics
21- => AppDomainDictionary . Get < ConcurrentDictionary < string , Diagnostic > > ( nameof ( Diagnostics ) ) ;
22-
23- /// <summary>
24- /// Creates a descriptor from well-known diagnostic kinds.
25- /// </summary>
26- /// <param name="sponsorable">The names of the sponsorable accounts that can be funded for the given product.</param>
27- /// <param name="product">The product or project developed by the sponsorable(s).</param>
28- /// <param name="prefix">Custom prefix to use for diagnostic IDs.</param>
29- /// <param name="status">The kind of status diagnostic to create.</param>
30- /// <returns>The given <see cref="DiagnosticDescriptor"/>.</returns>
31- /// <exception cref="NotImplementedException">The <paramref name="status"/> is not one of the known ones.</exception>
32- public DiagnosticDescriptor GetDescriptor ( string [ ] sponsorable , string product , string prefix , SponsorStatus status ) => status switch
24+ public static Dictionary < SponsorStatus , DiagnosticDescriptor > KnownDescriptors { get ; } = new ( )
3325 {
34- SponsorStatus . Unknown => CreateUnknown ( sponsorable , product , prefix ) ,
35- SponsorStatus . Sponsor => CreateSponsor ( sponsorable , prefix ) ,
36- SponsorStatus . Expiring => CreateExpiring ( sponsorable , prefix ) ,
37- SponsorStatus . Expired => CreateExpired ( sponsorable , prefix ) ,
38- _ => throw new NotImplementedException ( ) ,
26+ // Requires:
27+ // <Constant Include="Funding.Product" Value="[PRODUCT_NAME]" />
28+ // <Constant Include="Funding.AnalyzerPrefix" Value="[PREFIX]" />
29+ { SponsorStatus . Unknown , CreateUnknown ( [ .. Sponsorables . Keys ] , Funding . Product , Funding . Prefix ) } ,
30+ { SponsorStatus . Sponsor , CreateSponsor ( [ .. Sponsorables . Keys ] , Funding . Prefix ) } ,
31+ { SponsorStatus . Expiring , CreateExpiring ( [ .. Sponsorables . Keys ] , Funding . Prefix ) } ,
32+ { SponsorStatus . Expired , CreateExpired ( [ .. Sponsorables . Keys ] , Funding . Prefix ) } ,
3933 } ;
4034
4135 /// <summary>
42- /// Pushes a diagnostic for the given product. If an existing one exists, it is replaced .
36+ /// Acceses the diagnostics dictionary for the current <see cref="AppDomain"/> .
4337 /// </summary>
44- /// <returns>The same diagnostic that was pushed, for chained invocations.</returns>
45- public Diagnostic Push ( string product , Diagnostic diagnostic )
46- {
47- // Directly sets, since we only expect to get one warning per sponsorable+product
48- // combination.
49- Diagnostics [ product ] = diagnostic ;
50- return diagnostic ;
51- }
38+ ConcurrentDictionary < string , Diagnostic > Diagnostics
39+ => AppDomainDictionary . Get < ConcurrentDictionary < string , Diagnostic > > ( nameof ( Diagnostics ) ) ;
5240
5341 /// <summary>
5442 /// Attemps to remove a diagnostic for the given product.
@@ -62,17 +50,19 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
6250 }
6351
6452 /// <summary>
65- /// Gets the status of the given product based on a previously stored diagnostic.
53+ /// Gets the status of the given product based on a previously stored diagnostic.
54+ /// To ensure the value is always set before returning, use <see cref="GetOrSetStatus"/>.
55+ /// This method is safe to use (and would get a non-null value) in analyzers that run after CompilationStartAction(see
56+ /// https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md under Ordering of actions).
6657 /// </summary>
67- /// <param name="product">The product to check status for.</param>
6858 /// <returns>Optional <see cref="SponsorStatus"/> that was reported, if any.</returns>
69- public SponsorStatus ? GetStatus ( string product )
59+ public SponsorStatus ? GetStatus ( )
7060 {
7161 // NOTE: the SponsorLinkAnalyzer.SetStatus uses diagnostic properties to store the
7262 // kind of diagnostic as a simple string instead of the enum. We do this so that
7363 // multiple analyzers or versions even across multiple products, which all would
7464 // have their own enum, can still share the same diagnostic kind.
75- if ( Diagnostics . TryGetValue ( product , out var diagnostic ) &&
65+ if ( Diagnostics . TryGetValue ( Funding . Product , out var diagnostic ) &&
7666 diagnostic . Properties . TryGetValue ( nameof ( SponsorStatus ) , out var value ) )
7767 {
7868 // Switch on value matching DiagnosticKind names
@@ -89,7 +79,76 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
8979 return null ;
9080 }
9181
92- static DiagnosticDescriptor CreateSponsor ( string [ ] sponsorable , string prefix ) => new (
82+ /// <summary>
83+ /// Gets the status of the <see cref="Funding.Product"/>, or sets it from
84+ /// the given set of <paramref name="manifests"/> if not already set.
85+ /// </summary>
86+ public SponsorStatus GetOrSetStatus ( ImmutableArray < AdditionalText > manifests )
87+ => GetOrSetStatus ( ( ) => manifests ) ;
88+
89+ /// <summary>
90+ /// Gets the status of the <see cref="Funding.Product"/>, or sets it from
91+ /// the given analyzer <paramref name="options"/> if not already set.
92+ /// </summary>
93+ public SponsorStatus GetOrSetStatus ( Func < AnalyzerOptions ? > options )
94+ => GetOrSetStatus ( ( ) => options ( ) . GetSponsorManifests ( ) ) ;
95+
96+ SponsorStatus GetOrSetStatus ( Func < ImmutableArray < AdditionalText > > getManifests )
97+ {
98+ if ( GetStatus ( ) is { } status )
99+ return status ;
100+
101+ if ( ! SponsorLink . TryRead ( out var claims , getManifests ( ) . Select ( text =>
102+ ( text . GetText ( ) ? . ToString ( ) ?? "" , Sponsorables [ Path . GetFileNameWithoutExtension ( text . Path ) ] ) ) ) ||
103+ claims . GetExpiration ( ) is not DateTime exp )
104+ {
105+ // report unknown, either unparsed manifest or one with no expiration (which we never emit).
106+ Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Unknown ] , null ,
107+ properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Unknown ) ) ,
108+ Funding . Product , Sponsorables . Keys . Humanize ( Resources . Or ) ) ) ;
109+ return SponsorStatus . Unknown ;
110+ }
111+ else if ( exp < DateTime . Now )
112+ {
113+ // report expired or expiring soon if still within the configured days of grace period
114+ if ( exp . AddDays ( Funding . Grace ) < DateTime . Now )
115+ {
116+ // report expiring soon
117+ Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expiring ] , null ,
118+ properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Expiring ) ) ) ) ;
119+ return SponsorStatus . Expiring ;
120+ }
121+ else
122+ {
123+ // report expired
124+ Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expired ] , null ,
125+ properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Expired ) ) ) ) ;
126+ return SponsorStatus . Expired ;
127+ }
128+ }
129+ else
130+ {
131+ // report sponsor
132+ Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Sponsor ] , null ,
133+ properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Sponsor ) ) ,
134+ Funding . Product ) ) ;
135+ return SponsorStatus . Sponsor ;
136+ }
137+ }
138+
139+ /// <summary>
140+ /// Pushes a diagnostic for the given product.
141+ /// </summary>
142+ /// <returns>The same diagnostic that was pushed, for chained invocations.</returns>
143+ Diagnostic Push ( string product , Diagnostic diagnostic )
144+ {
145+ // We only expect to get one warning per sponsorable+product
146+ // combination, and first one to set wins.
147+ Diagnostics . TryAdd ( product , diagnostic ) ;
148+ return diagnostic ;
149+ }
150+
151+ internal static DiagnosticDescriptor CreateSponsor ( string [ ] sponsorable , string prefix ) => new (
93152 $ "{ prefix } 100",
94153 Resources . Sponsor_Title ,
95154 Resources . Sponsor_Message ,
@@ -100,7 +159,7 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
100159 helpLinkUri : "https://github.com/devlooped#sponsorlink" ,
101160 "DoesNotSupportF1Help" ) ;
102161
103- static DiagnosticDescriptor CreateUnknown ( string [ ] sponsorable , string product , string prefix ) => new (
162+ internal static DiagnosticDescriptor CreateUnknown ( string [ ] sponsorable , string product , string prefix ) => new (
104163 $ "{ prefix } 101",
105164 Resources . Unknown_Title ,
106165 Resources . Unknown_Message ,
@@ -113,7 +172,7 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
113172 helpLinkUri : "https://github.com/devlooped#sponsorlink" ,
114173 WellKnownDiagnosticTags . NotConfigurable ) ;
115174
116- static DiagnosticDescriptor CreateExpiring ( string [ ] sponsorable , string prefix ) => new (
175+ internal static DiagnosticDescriptor CreateExpiring ( string [ ] sponsorable , string prefix ) => new (
117176 $ "{ prefix } 103",
118177 Resources . Expiring_Title ,
119178 Resources . Expiring_Message ,
@@ -124,7 +183,7 @@ public Diagnostic Push(string product, Diagnostic diagnostic)
124183 helpLinkUri : "https://github.com/devlooped#autosync" ,
125184 "DoesNotSupportF1Help" , WellKnownDiagnosticTags . NotConfigurable ) ;
126185
127- static DiagnosticDescriptor CreateExpired ( string [ ] sponsorable , string prefix ) => new (
186+ internal static DiagnosticDescriptor CreateExpired ( string [ ] sponsorable , string prefix ) => new (
128187 $ "{ prefix } 104",
129188 Resources . Expired_Title ,
130189 Resources . Expired_Message ,
0 commit comments