Skip to content

Commit

Permalink
After grace period, emit code with warnings
Browse files Browse the repository at this point in the history
It was made quite clear that (some?) folks would rather just disable our friendly warning rather than sponsor. So we'll instead just make sponsorship mandatory.

See #352 (comment)

I worked hard to accomodate very flexible options for sponsoring, and this project ain't maintaining itself. It's fine if folks use something else. I made this initially just for myself, and I'm glad it's been useful for others.

We also add remarks to the emitted APIs during the grace period, so it's not surprising they turn to warnings later.
  • Loading branch information
kzu committed Sep 15, 2024
1 parent 5f76eba commit d4ccfb8
Show file tree
Hide file tree
Showing 16 changed files with 236 additions and 43 deletions.
5 changes: 4 additions & 1 deletion src/Directory.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@

> This project uses SponsorLink and may issue IDE-only warnings if no active sponsorship is detected. Learn more at https://github.com/devlooped#sponsorlink.
</Description>
<IsAnalyzer>$(PackFolder.StartsWith('analyzers/'))</IsAnalyzer>
</PropertyGroup>

<ItemGroup>
<NoneWithoutCopyToOutputDirectory Include="@(None)" Exclude="@(None -> HasMetadata('CopyToOutputDirectory'))" />
<None Update="@(NoneWithoutCopyToOutputDirectory)" CopyToOutputDirectory="PreserveNewest" />
<EmbeddedResource Include="@(None -> WithMetadataValue('Extension', '.sbntxt'))" />
<None Update="@(None -> WithMetadataValue('Extension', '.sbntxt'))" Pack="true" />
</ItemGroup>

<ItemGroup Condition="'$(IsAnalyzer)' == 'true'">
<PackageFile Include="$(MSBuildThisFileDirectory)_._" PackagePath="lib/netstandard2.0/_._" />
<PackageFile Include="*.props;*.targets" PackagePath="build\$(TargetFramework)\%(Filename)%(Extension)" Visible="true" />
</ItemGroup>

<Target Name="PackCopyLocalLockFileAssemblies" Condition="'$(MergeAnalyzerAssemblies)' != 'true'" BeforeTargets="GetPackageContents" DependsOnTargets="ReferenceCopyLocalPathsOutputGroup">
<Target Name="PackCopyLocalLockFileAssemblies" Condition="'$(IsAnalyzer)' == 'true' and '$(MergeAnalyzerAssemblies)' != 'true'" BeforeTargets="GetPackageContents" DependsOnTargets="ReferenceCopyLocalPathsOutputGroup">
<ItemGroup>
<ReferenceCopyLocalAssemblies Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'
And !$([MSBuild]::ValueOrDefault('%(FileName)', '').EndsWith('.resources', StringComparison.OrdinalIgnoreCase))
Expand Down
15 changes: 14 additions & 1 deletion src/Shared/EmbeddedResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@
using System.Linq;
using System.Reflection;

static class EmbeddedResource
/// <summary>
/// Allows easy access to embedded resources.
/// </summary>
public static class EmbeddedResource
{
#if DEBUG
static readonly string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "";
#endif

/// <summary>
/// Gets the content of the embedded resource at the specified relative path.
/// </summary>
public static string GetContent(string relativePath)
{
using var stream = GetStream(relativePath);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}

/// <summary>
/// Gets the bytes of the embedded resource at the specified relative path.
/// </summary>
public static byte[] GetBytes(string relativePath)
{
using var stream = GetStream(relativePath);
Expand All @@ -25,6 +34,10 @@ public static byte[] GetBytes(string relativePath)
return bytes;
}

/// <summary>
/// Gets the stream of the embedded resource at the specified relative path.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public static Stream GetStream(string relativePath)
{
#if DEBUG
Expand Down
9 changes: 8 additions & 1 deletion src/Shared/PathSanitizer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
using System.Text.RegularExpressions;

static class PathSanitizer
/// <summary>
/// Sanitizes paths for use as identifiers.
/// </summary>
public static class PathSanitizer
{
static readonly Regex invalidCharsRegex = new(@"\W");

/// <summary>
/// Sanitizes the specified path for use as an identifier.
/// </summary>
public static string Sanitize(string path, string parent)
{
var partStr = invalidCharsRegex.Replace(path, "_");
Expand Down
26 changes: 22 additions & 4 deletions src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -74,11 +79,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}

static void GenerateSource(SourceProductionContext spc,
(ImmutableArray<KeyValuePair<string, string>> attributes, (string? ns, ParseOptions parse)) arg)
(ImmutableArray<KeyValuePair<string, string>> 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;

Expand Down
27 changes: 22 additions & 5 deletions src/ThisAssembly.AssemblyInfo/CSharp.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.CodeDom.Compiler;
using System.Runtime.CompilerServices;
{{ if Namespace }}
Expand All @@ -21,19 +22,35 @@ partial class ThisAssembly
/// <summary>
/// Gets the AssemblyInfo attributes.
/// </summary>
{{~ if Remarks ~}}
{{ Remarks }}
/// <see cref="{{ Url }}"/>
{{~ end ~}}
[GeneratedCode("ThisAssembly.AssemblyInfo", "{{ Version }}")]
[CompilerGenerated]
public static partial class Info
{
{{~ for prop in Properties ~}}
{{- for prop in Properties ~}}

{{~ if Remarks ~}}
{{ Remarks }}
/// <see cref="{{ Url }}"/>
{{~ 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 ~}}
}
}
}
3 changes: 3 additions & 0 deletions src/ThisAssembly.AssemblyInfo/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public Model(IEnumerable<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> Properties { get; }
}
22 changes: 20 additions & 2 deletions src/ThisAssembly.Constants/CSharp.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,38 @@
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
{{ func summary }}
{{- func summary -}}
/// <summary>
{{~ if $0.Comment ~}}
/// {{ $0.Comment }}
{{~ else ~}}
/// => @"{{ $0.Value }}"
{{~ end ~}}
/// </summary>
{{- end -}}
{{ func obsolete }}
{{~ if Warn ~}}
[Obsolete("{{ Warn }}", false
#if NET6_0_OR_GREATER
, UrlFormat = "{{ Url }}"
#endif
)]

{{~ end }}
{{ end }}
{{ func remarks }}
{{~ if Remarks ~}}
{{ Remarks }}
/// <see cref="{{ Url }}"/>
{{~ 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 " " "_" }} =
"""
Expand Down
48 changes: 34 additions & 14 deletions src/ThisAssembly.Constants/ConstantsGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) &&
Expand All @@ -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));
}
Expand Down
3 changes: 3 additions & 0 deletions src/ThisAssembly.Constants/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand Down
16 changes: 14 additions & 2 deletions src/ThisAssembly.Strings/CSharp.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@
/// = "{{ $0.Value }}"
{{~ end ~}}
/// </summary>
{{~ if Remarks ~}}
{{ Remarks }}
/// <see cref="{{ Url }}"/>
{{~ 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 }}");
Expand Down Expand Up @@ -70,7 +81,7 @@ using System;
using System.Globalization;
{{ if Namespace }}
namespace {{ Namespace }};
{{~ end ~}}
{{ end }}

/// <summary>
/// Provides access to the current assembly information.
Expand All @@ -80,5 +91,6 @@ partial class ThisAssembly
/// <summary>
/// Provides access to the assembly strings.
/// </summary>
{{- remarks -}}
{{ render RootArea ResourceName }}
}
7 changes: 5 additions & 2 deletions src/ThisAssembly.Strings/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ResourceArea> NestedAreas { get; init; } = new List<ResourceArea>();
public List<ResourceValue> Values { get; init; } = new List<ResourceValue>();
public List<ResourceArea> NestedAreas { get; init; } = [];
public List<ResourceValue> Values { get; init; } = [];
}

[DebuggerDisplay("{Id} = {Value}")]
Expand Down
Loading

0 comments on commit d4ccfb8

Please sign in to comment.