diff --git a/src/Directory.targets b/src/Directory.targets index 190069e0..d2a8362b 100644 --- a/src/Directory.targets +++ b/src/Directory.targets @@ -5,6 +5,7 @@ > This project uses SponsorLink and may issue IDE-only warnings if no active sponsorship is detected. Learn more at https://github.com/devlooped#sponsorlink. + $(PackFolder.StartsWith('analyzers/')) @@ -12,12 +13,14 @@ + + - + + /// Gets the content of the embedded resource at the specified relative path. + /// public static string GetContent(string relativePath) { using var stream = GetStream(relativePath); @@ -17,6 +23,9 @@ public static string GetContent(string relativePath) return reader.ReadToEnd(); } + /// + /// Gets the bytes of the embedded resource at the specified relative path. + /// public static byte[] GetBytes(string relativePath) { using var stream = GetStream(relativePath); @@ -25,6 +34,10 @@ public static byte[] GetBytes(string relativePath) return bytes; } + /// + /// Gets the stream of the embedded resource at the specified relative path. + /// + /// public static Stream GetStream(string relativePath) { #if DEBUG diff --git a/src/Shared/PathSanitizer.cs b/src/Shared/PathSanitizer.cs index 992cda7b..ff8b0d29 100644 --- a/src/Shared/PathSanitizer.cs +++ b/src/Shared/PathSanitizer.cs @@ -1,8 +1,15 @@ using System.Text.RegularExpressions; -static class PathSanitizer +/// +/// Sanitizes paths for use as identifiers. +/// +public static class PathSanitizer { static readonly Regex invalidCharsRegex = new(@"\W"); + + /// + /// Sanitizes the specified path for use as an identifier. + /// public static string Sanitize(string path, string parent) { var partStr = invalidCharsRegex.Replace(path, "_"); diff --git a/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs b/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs index ffbe6e28..d5e3ec6a 100644 --- a/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs +++ b/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs @@ -1,14 +1,19 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Threading; +using Devlooped.Sponsors; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; using Scriban; +using static Devlooped.Sponsors.SponsorLink; +using Resources = Devlooped.Sponsors.Resources; namespace ThisAssembly; @@ -41,7 +46,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Read the ThisAssemblyNamespace property or default to null var right = context.AnalyzerConfigOptionsProvider .Select((c, t) => c.GlobalOptions.TryGetValue("build_property.ThisAssemblyNamespace", out var ns) && !string.IsNullOrEmpty(ns) ? ns : null) - .Combine(context.ParseOptionsProvider); + .Combine(context.ParseOptionsProvider.Combine(context.GetStatusOptions())); context.RegisterSourceOutput( metadata.Combine(right), @@ -74,11 +79,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } static void GenerateSource(SourceProductionContext spc, - (ImmutableArray> attributes, (string? ns, ParseOptions parse)) arg) + (ImmutableArray> attributes, (string? ns, (ParseOptions parse, StatusOptions options))) arg) { - var (attributes, (ns, parse)) = arg; + var (attributes, (ns, (parse, options))) = arg; + var model = new Model([.. attributes], ns); + if (IsEditor) + { + var status = Diagnostics.GetOrSetStatus(options); + if (status == SponsorStatus.Unknown || status == SponsorStatus.Expired) + { + model.Warn = string.Format(CultureInfo.CurrentCulture, Resources.Editor_Disabled, Funding.Product, Funding.HelpUrl); + model.Remarks = Resources.Editor_DisabledRemarks; + } + else if (status == SponsorStatus.Grace && Diagnostics.TryGet() is { } grace && grace.Properties.TryGetValue(nameof(SponsorStatus.Grace), out var days)) + { + model.Remarks = string.Format(CultureInfo.CurrentCulture, Resources.Editor_GraceRemarks, days); + } + } - var model = new Model(attributes.ToList(), ns); if (parse is CSharpParseOptions cs && (int)cs.LanguageVersion >= 1100) model.RawStrings = true; diff --git a/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt b/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt index c4c7ed08..f2d3d736 100644 --- a/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt +++ b/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt @@ -7,6 +7,7 @@ // //------------------------------------------------------------------------------ +using System; using System.CodeDom.Compiler; using System.Runtime.CompilerServices; {{ if Namespace }} @@ -21,19 +22,35 @@ partial class ThisAssembly /// /// Gets the AssemblyInfo attributes. /// + {{~ if Remarks ~}} + {{ Remarks }} + /// + {{~ end ~}} [GeneratedCode("ThisAssembly.AssemblyInfo", "{{ Version }}")] [CompilerGenerated] public static partial class Info { - {{~ for prop in Properties ~}} + {{- for prop in Properties ~}} + + {{~ if Remarks ~}} + {{ Remarks }} + /// + {{~ end ~}} + {{~ if Warn ~}} + [Obsolete("{{ Warn }}", false + #if NET6_0_OR_GREATER + , UrlFormat = "{{ Url }}" + #endif + )] + {{~ end ~}} {{~ if RawStrings ~}} public const string {{ prop.Key }} = -""" -{{ prop.Value }} -"""; + """ + {{ prop.Value }} + """; {{~ else ~}} public const string {{ prop.Key }} = @"{{ prop.Value }}"; {{~ end ~}} {{~ end ~}} } -} \ No newline at end of file +} diff --git a/src/ThisAssembly.AssemblyInfo/Model.cs b/src/ThisAssembly.AssemblyInfo/Model.cs index da0ab9c2..1a988760 100644 --- a/src/ThisAssembly.AssemblyInfo/Model.cs +++ b/src/ThisAssembly.AssemblyInfo/Model.cs @@ -13,6 +13,9 @@ public Model(IEnumerable> properties, string? ns) public string? Namespace { get; } public bool RawStrings { get; set; } = false; public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); + public string Url => Devlooped.Sponsors.SponsorLink.Funding.HelpUrl; + public string? Warn { get; set; } + public string? Remarks { get; set; } public List> Properties { get; } } diff --git a/src/ThisAssembly.Constants/CSharp.sbntxt b/src/ThisAssembly.Constants/CSharp.sbntxt index a5af0287..e0231946 100644 --- a/src/ThisAssembly.Constants/CSharp.sbntxt +++ b/src/ThisAssembly.Constants/CSharp.sbntxt @@ -8,7 +8,7 @@ // the code is regenerated. // //------------------------------------------------------------------------------ -{{ func summary }} +{{- func summary -}} /// {{~ if $0.Comment ~}} /// {{ $0.Comment }} @@ -16,12 +16,30 @@ /// => @"{{ $0.Value }}" {{~ end ~}} /// +{{- end -}} +{{ func obsolete }} +{{~ if Warn ~}} +[Obsolete("{{ Warn }}", false +#if NET6_0_OR_GREATER + , UrlFormat = "{{ Url }}" +#endif +)] + +{{~ end }} +{{ end }} +{{ func remarks }} +{{~ if Remarks ~}} +{{ Remarks }} +/// +{{~ end ~}} {{ end }} {{ func render }} public static partial class {{ $0.Name | string.replace "-" "_" | string.replace " " "_" }} { {{~ for value in $0.Values ~}} - {{- summary value ~}} + {{- summary value -}} + {{- remarks -}} + {{ obsolete }} {{~ if RawStrings ~}} public const string {{ value.Name | string.replace "-" "_" | string.replace " " "_" }} = """ diff --git a/src/ThisAssembly.Constants/ConstantsGenerator.cs b/src/ThisAssembly.Constants/ConstantsGenerator.cs index a8aa44ee..c865fdd7 100644 --- a/src/ThisAssembly.Constants/ConstantsGenerator.cs +++ b/src/ThisAssembly.Constants/ConstantsGenerator.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Globalization; using System.IO; using System.Linq; using System.Text; +using Devlooped.Sponsors; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; @@ -58,15 +60,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select((c, t) => c.GlobalOptions.TryGetValue("build_property.ThisAssemblyNamespace", out var ns) && !string.IsNullOrEmpty(ns) ? ns : null) .Combine(context.ParseOptionsProvider); - context.RegisterSourceOutput( - files.Combine(right), - GenerateConstant); + var inputs = files.Combine(right); + // this is required to ensure status is registered properly independently of analyzer runs. + var options = context.GetStatusOptions(); + context.RegisterSourceOutput(inputs.Combine(options), GenerateConstant); + //(spc, source) => + //{ + // var status = Diagnostics.GetOrSetStatus(source.Right); + // var warn = IsEditor && + // (status == Devlooped.Sponsors.SponsorStatus.Unknown || status == Devlooped.Sponsors.SponsorStatus.Expired); + + // GenerateConstant(spc, source.Left, warn ? string.Format( + // CultureInfo.CurrentCulture, Resources.Editor_Disabled, Funding.Product, Funding.HelpUrl) : null); + //}); } - void GenerateConstant(SourceProductionContext spc, ((string name, string value, string? comment, string root), (string? ns, ParseOptions parse)) args) + void GenerateConstant(SourceProductionContext spc, + (((string name, string value, string? comment, string root), (string? ns, ParseOptions parse)), StatusOptions options) args) { - var ((name, value, comment, root), (ns, parse)) = args; + var (((name, value, comment, root), (ns, parse)), options) = args; var cs = (CSharpParseOptions)parse; if (!string.IsNullOrWhiteSpace(ns) && @@ -87,24 +100,31 @@ void GenerateConstant(SourceProductionContext spc, ((string name, string value, if ((int)cs.LanguageVersion >= 1100) model.RawStrings = true; + if (IsEditor) + { + var status = Diagnostics.GetOrSetStatus(options); + if (status == SponsorStatus.Unknown || status == SponsorStatus.Expired) + { + model.Warn = string.Format(CultureInfo.CurrentCulture, Resources.Editor_Disabled, Funding.Product, Funding.HelpUrl); + model.Remarks = Resources.Editor_DisabledRemarks; + } + else if (status == SponsorStatus.Grace && Diagnostics.TryGet() is { } grace && grace.Properties.TryGetValue(nameof(SponsorStatus.Grace), out var days)) + { + model.Remarks = string.Format(CultureInfo.CurrentCulture, Resources.Editor_GraceRemarks, days); + } + } + var output = template.Render(model, member => member.Name); // Apply formatting since indenting isn't that nice in Scriban when rendering nested // structures via functions. if (parse.Language == LanguageNames.CSharp) { - output = SyntaxFactory.ParseCompilationUnit(output) + output = SyntaxFactory.ParseCompilationUnit(output, options: cs) .NormalizeWhitespace() .GetText() .ToString(); } - //else if (language == LanguageNames.VisualBasic) - //{ - // output = Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory.ParseCompilationUnit(output) - // .NormalizeWhitespace() - // .GetText() - // .ToString(); - //} spc.AddSource($"{root}.{name}.g.cs", SourceText.From(output, Encoding.UTF8)); } diff --git a/src/ThisAssembly.Constants/Model.cs b/src/ThisAssembly.Constants/Model.cs index 8945f776..9e7f3cde 100644 --- a/src/ThisAssembly.Constants/Model.cs +++ b/src/ThisAssembly.Constants/Model.cs @@ -13,6 +13,9 @@ record Model(Area RootArea, string? Namespace) { public bool RawStrings { get; set; } = false; public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); + public string Url => Devlooped.Sponsors.SponsorLink.Funding.HelpUrl; + public string? Warn { get; set; } + public string? Remarks { get; set; } } [DebuggerDisplay("Name = {Name}, NestedAreas = {NestedAreas.Count}, Values = {Values.Count}")] diff --git a/src/ThisAssembly.Strings/CSharp.sbntxt b/src/ThisAssembly.Strings/CSharp.sbntxt index 039d67b6..2a467d9a 100644 --- a/src/ThisAssembly.Strings/CSharp.sbntxt +++ b/src/ThisAssembly.Strings/CSharp.sbntxt @@ -16,11 +16,22 @@ /// = "{{ $0.Value }}" {{~ end ~}} /// + {{~ if Remarks ~}} + {{ Remarks }} + /// + {{~ end ~}} + {{~ if Warn ~}} + [Obsolete("{{ Warn }}", false + #if NET6_0_OR_GREATER + , UrlFormat = "{{ Url }}" + #endif + )] + {{~ end ~}} {{ end }} {{ func render }} public static partial class {{ $0.Id }} { - {{~ for value in $0.Values ~}} + {{~ for value in $0.Values }} {{~ if!(value.HasFormat) ~}} {{- summary value ~}} public static string {{ value.Id }} => Strings.GetResourceManager("{{ $1 }}").GetString("{{ value.Name }}"); @@ -70,7 +81,7 @@ using System; using System.Globalization; {{ if Namespace }} namespace {{ Namespace }}; -{{~ end ~}} +{{ end }} /// /// Provides access to the current assembly information. @@ -80,5 +91,6 @@ partial class ThisAssembly /// /// Provides access to the assembly strings. /// + {{- remarks -}} {{ render RootArea ResourceName }} } \ No newline at end of file diff --git a/src/ThisAssembly.Strings/Model.cs b/src/ThisAssembly.Strings/Model.cs index 46475a61..2169d992 100644 --- a/src/ThisAssembly.Strings/Model.cs +++ b/src/ThisAssembly.Strings/Model.cs @@ -10,6 +10,9 @@ record Model(ResourceArea RootArea, string ResourceName, string? Namespace) { public string? Version => Assembly.GetExecutingAssembly().GetName().Version?.ToString(3); + public string Url => Devlooped.Sponsors.SponsorLink.Funding.HelpUrl; + public string? Warn { get; set; } + public string? Remarks { get; set; } } static class ResourceFile @@ -131,8 +134,8 @@ static ResourceValue GetValue(string resourceId, string resourceName, string res [DebuggerDisplay("Id = {Id}, NestedAreas = {NestedAreas.Count}, Values = {Values.Count}")] record ResourceArea(string Id, string Prefix) { - public List NestedAreas { get; init; } = new List(); - public List Values { get; init; } = new List(); + public List NestedAreas { get; init; } = []; + public List Values { get; init; } = []; } [DebuggerDisplay("{Id} = {Value}")] diff --git a/src/ThisAssembly.Strings/StringsGenerator.cs b/src/ThisAssembly.Strings/StringsGenerator.cs index 73aae59f..97945aba 100644 --- a/src/ThisAssembly.Strings/StringsGenerator.cs +++ b/src/ThisAssembly.Strings/StringsGenerator.cs @@ -1,11 +1,15 @@ using System; +using System.Globalization; using System.IO; using System.Linq; using System.Resources; using System.Text; +using Devlooped.Sponsors; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using Scriban; +using static Devlooped.Sponsors.SponsorLink; namespace ThisAssembly; @@ -46,24 +50,37 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput( - files.Combine(right), + files.Combine(right).Combine(context.ParseOptionsProvider.Combine(context.GetStatusOptions())), GenerateSource); } static void GenerateSource(SourceProductionContext spc, - ((string fileName, SourceText? text, string resourceName), (string? ns, string language)) arg) + (((string fileName, SourceText? text, string resourceName), (string? ns, string language)),(ParseOptions parse, StatusOptions options)) arg) { - var ((fileName, resourceText, resourceName), (ns, language)) = arg; + var (((fileName, resourceText, resourceName), (ns, language)), (parse, options)) = arg; var file = language.Replace("#", "Sharp") + ".sbntxt"; var template = Template.Parse(EmbeddedResource.GetContent(file), file); var rootArea = ResourceFile.LoadText(resourceText!.ToString(), "Strings"); var model = new Model(rootArea, resourceName, ns); + if (IsEditor) + { + var status = Diagnostics.GetOrSetStatus(options); + if (status == SponsorStatus.Unknown || status == SponsorStatus.Expired) + { + model.Warn = string.Format(CultureInfo.CurrentCulture, Resources.Editor_Disabled, Funding.Product, Funding.HelpUrl); + model.Remarks = Resources.Editor_DisabledRemarks; + } + else if (status == SponsorStatus.Grace && Diagnostics.TryGet() is { } grace && grace.Properties.TryGetValue(nameof(SponsorStatus.Grace), out var days)) + { + model.Remarks = string.Format(CultureInfo.CurrentCulture, Resources.Editor_GraceRemarks, days); + } + } var output = template.Render(model, member => member.Name); - output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output) + output = SyntaxFactory.ParseCompilationUnit(output, options: parse as CSharpParseOptions) .NormalizeWhitespace() .GetText() .ToString(); diff --git a/src/ThisAssembly.Tests/Funding.cs b/src/ThisAssembly.Tests/Funding.cs new file mode 100644 index 00000000..dc856909 --- /dev/null +++ b/src/ThisAssembly.Tests/Funding.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Devlooped.Sponsors; + +partial class SponsorLink +{ + public partial class Funding + { + public const string HelpUrl = "https://github.com/devlooped#sponsorlink"; + } +} diff --git a/src/ThisAssembly.Tests/ScribanTests.cs b/src/ThisAssembly.Tests/ScribanTests.cs new file mode 100644 index 00000000..c0831e46 --- /dev/null +++ b/src/ThisAssembly.Tests/ScribanTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Scriban; +using Xunit; +using Xunit.Abstractions; + +namespace ThisAssemblyTests; + +public class ScribanTests(ITestOutputHelper Console) +{ + [Fact] + public void CanRenderModel() + { + var source = + """ + {{ func remarks }} + /// + {{ end }} + /// + /// {{ summary }} + /// + {{ remarks }} + """; + + var template = Template.Parse(source); + var output = template.Render(new + { + summary = "This is a summary", + url = "https://github.com/devlooped#sponsorlink" + }); + + Assert.Contains("https://github.com/devlooped#sponsorlink", output); + Console.WriteLine(output); + } +} diff --git a/src/ThisAssembly.Tests/Tests.cs b/src/ThisAssembly.Tests/Tests.cs index b29f5a85..3b596946 100644 --- a/src/ThisAssembly.Tests/Tests.cs +++ b/src/ThisAssembly.Tests/Tests.cs @@ -1,8 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using System.IO; -using Devlooped; using Xunit; using Xunit.Abstractions; +//using ThisAssembly = ThisAssemblyTests [assembly: SuppressMessage("SponsorLink", "SL04")] @@ -10,7 +11,6 @@ namespace ThisAssemblyTests; public record class Tests(ITestOutputHelper Output) { - DateTime dxt = DateTime.Now; [Fact] public void CanReadResourceFile() => Assert.NotNull(ResourceFile.Load("Resources.resx", "Strings")); diff --git a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj index 6e72472b..d5b132b7 100644 --- a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj +++ b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj @@ -3,7 +3,7 @@ false net8.0 - Devlooped + ThisAssemblyTests A Description with a newline and @@ -17,10 +17,11 @@ net472 ThisAssemblyTests true - CS8981;$(NoWarn) + CS0618;CS8981;TA100;$(NoWarn) + false - + @@ -40,6 +41,7 @@ + @@ -81,7 +83,7 @@ - + @@ -93,4 +95,12 @@ + + + $(Version) + + + + +