Skip to content

Commit

Permalink
Update to latest SL analyzer from new upstream location
Browse files Browse the repository at this point in the history
  • Loading branch information
kzu committed Jul 8, 2024
1 parent 9a386b5 commit d53f442
Show file tree
Hide file tree
Showing 34 changed files with 605 additions and 343 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/os-matrix.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["windows-latest", "ubuntu-latest", "macOS-latest"]
[ "ubuntu-latest", "macOS-latest", "windows-latest" ]
242 changes: 137 additions & 105 deletions .netconfig

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Directory.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<PropertyGroup Label="NuGet">
<PackageTags>dotnet roslyn</PackageTags>
<PackOnBuild>true</PackOnBuild>
<SignAssembly>false</SignAssembly>
<DevelopmentDependency>true</DevelopmentDependency>
<ThisAssemblyMinimumRoslynVersion>4.0</ThisAssemblyMinimumRoslynVersion>
<PackFolder>analyzers/dotnet/roslyn$(ThisAssemblyMinimumRoslynVersion)/cs</PackFolder>
Expand Down
21 changes: 15 additions & 6 deletions src/SponsorLink/Analyzer/Analyzer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
<IsRoslynComponent>true</IsRoslynComponent>
<PackFolder>analyzers/dotnet/roslyn4.0</PackFolder>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<MergeAnalyzerAssemblies>false</MergeAnalyzerAssemblies>
<MergeAnalyzerAssemblies Condition="'$(CI)' == 'true'">true</MergeAnalyzerAssemblies>
<CustomAfterMicrosoftCSharpTargets>$(MSBuildThisFileDirectory)..\SponsorLink.targets</CustomAfterMicrosoftCSharpTargets>
<MergeAnalyzerAssemblies>true</MergeAnalyzerAssemblies>
<ImplicitUsings>disable</ImplicitUsings>
<FundingPackageId>SponsorableLib</FundingPackageId>
</PropertyGroup>

<ItemGroup>
Expand All @@ -22,16 +24,23 @@
<PackageReference Include="ThisAssembly.Project" Version="1.4.3" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Tests" />
</ItemGroup>

<ItemGroup>
<None Update="buildTransitive\SponsorableLib.targets" Pack="true" />
</ItemGroup>

<ItemGroup>
<Compile Remove="C:\Code\devlooped.oss\src\SponsorLink\SponsorLink\ThisAssembly.cs" />
<InternalsVisibleTo Include="Tests" />
</ItemGroup>

<!-- To support tests, fake an extra sponsorable with the test key -->
<Target Name="ReadTestJwk" BeforeTargets="GetAssemblyAttributes">
<PropertyGroup>
<!-- Read public key we validate manifests against -->
<TestJwk>$([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\Tests\keys\kzu.pub.jwk'))</TestJwk>
</PropertyGroup>
<ItemGroup>
<AssemblyMetadata Include="Funding.GitHub.kzu" Value="$(TestJwk)" />
</ItemGroup>
</Target>

</Project>
7 changes: 5 additions & 2 deletions src/SponsorLink/Analyzer/StatusReportingGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ public class StatusReportingGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterSourceOutput(
context.GetSponsorManifests(),
// this is required to ensure status is registered properly independently
// of analyzer runs.
context.GetSponsorAdditionalFiles().Combine(context.AnalyzerConfigOptionsProvider),
(spc, source) =>
{
var status = Diagnostics.GetOrSetStatus(source);
var (manifests, options) = source;
var status = Diagnostics.GetOrSetStatus(manifests, options);
spc.AddSource("StatusReporting.cs", $"// Status: {status}");
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
<Import Project="Devlooped.Sponsors.targets"/>
<ItemGroup>
<!-- Brings in the analyzer file to report installation time -->
<SponsorablePackageId Include="SponsorableLib" />
<FundingPackageId Include="SponsorableLib" />
</ItemGroup>
</Project>
20 changes: 0 additions & 20 deletions src/SponsorLink/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,4 @@
<Product>SponsorableLib</Product>
</PropertyGroup>

<ItemGroup>
<!--<Constant Include="Funding.Product" Value="$(Product)" />
<Constant Include="Funding.AnalyzerPrefix" Value="SLIB" />-->
<!--<Constant Include="Funding.GraceDays" Value="21" />-->
</ItemGroup>

<!-- DOGFOODING LOCAL BUILDS -->
<!-- Create a Directory.targets.user alongside this file, with the following content
(update the version number to the number of the built local package): -->
<!--
<Project>
<ItemGroup Condition="Exists('$(DevPath)')">
<PackageReference Update="@(PackageReference -> WithMetadataValue('Identity', 'Devlooped.SponsorLink'))"
Version="42.42.6587" />
</ItemGroup>
</Project>
-->

</Project>
24 changes: 18 additions & 6 deletions src/SponsorLink/SponsorLink.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
<PropertyGroup>
<ShowSponsorLinkInProject Condition="$(ShowSponsorLinkInProject) == '' and '$(TargetFramework)' == 'netstandard2.0'">true</ShowSponsorLinkInProject>
<!-- This ensures we expose only the main assembly in the Dependencies > Analyzers node -->
<MergeAnalyzerAssemblies Condition="'$(MergeAnalyzerAssemblies)' == '' and '$(Configuration)' == 'Release'">true</MergeAnalyzerAssemblies>
<MergeAnalyzerAssemblies Condition="'$(MergeAnalyzerAssemblies)' == '' and '$(Configuration)' == 'Release' and '$(OS)' == 'Windows_NT'">true</MergeAnalyzerAssemblies>
<MergeAnalyzerAssemblies Condition="'$(MergeAnalyzerAssemblies)' == ''">false</MergeAnalyzerAssemblies>
<!-- Whether we merge files or not, dependencies will need to be copy-local -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Make Resources visible to intellisense -->
<CoreCompileDependsOn>CoreResGen;$(CoreCompileDependsOn)</CoreCompileDependsOn>

<!-- Default funding product the Product, which already part of ThisAssembly -->
<FundingProduct Condition="'$(FundingProduct)' == ''">$(Product)</FundingProduct>
<FundingPackageId Condition="'$(FundingPackageId)' == ''">$(PackageId)</FundingPackageId>
<!-- Default prefix is the joined upper-case letters in the product name (i.e. for ThisAssembly, TA) -->
<FundingPrefix Condition="'$(FundingPrefix)' == ''">$([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", ""))</FundingPrefix>
<!-- Default grace days for an expired sponsor manifest or unknown status -->
Expand Down Expand Up @@ -68,28 +70,38 @@
</None>
</ItemGroup>

<PropertyGroup>
<PackMergedAssemblies>true</PackMergedAssemblies>
<PackMergedAssemblies Condition="'$(MergeAnalyzerAssemblies)' == 'true'">false</PackMergedAssemblies>
</PropertyGroup>

<ItemGroup Condition="'$(ManagePackageVersionsCentrally)' == 'true'">
<PackageReference Include="Humanizer.Core" VersionOverride="2.14.1" PrivateAssets="all" Pack="false" />
<PackageReference Include="Humanizer.Core" VersionOverride="2.14.1" PrivateAssets="all" Pack="$(PackMergedAssemblies)" />
<PackageReference Include="Humanizer.Core.es" VersionOverride="2.14.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" VersionOverride="7.6.2" PrivateAssets="all" Pack="false" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" VersionOverride="7.6.2" PrivateAssets="all" Pack="$(PackMergedAssemblies)" />
<PackageReference Include="ILRepack" Version="2.0.33" VersionOverride="all" PrivateAssets="all" Pack="false" />
</ItemGroup>

<ItemGroup Condition="'$(ManagePackageVersionsCentrally)' != 'true'">
<PackageReference Include="Humanizer.Core" Version="2.14.1" PrivateAssets="all" Pack="false" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" PrivateAssets="all" Pack="$(PackMergedAssemblies)" />
<PackageReference Include="Humanizer.Core.es" Version="2.14.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.6.2" PrivateAssets="all" Pack="false" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.6.2" PrivateAssets="all" Pack="$(PackMergedAssemblies)" />
<PackageReference Include="ILRepack" Version="2.0.33" PrivateAssets="all" Pack="false" />
</ItemGroup>

<Target Name="EmitFunding" BeforeTargets="CompileDesignTime;CoreCompile" Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)SponsorLink.g.cs">
<Warning Condition="'$(FundingPackageId)' == ''" Code="SL001"
Text="Could not determine value of FundingPackageId (defaulted to PackageId). Defaulting it to FundingProduct ('$(FundingProduct)'). Make sure this matches the containing package id, or set an explicit value." />
<PropertyGroup>
<!-- Default to Product, which is most common for single-package products (i.e. Moq) -->
<FundingPackageId Condition="'$(FundingPackageId)' == ''">$(FundingProduct)</FundingPackageId>
<SponsorLinkPartial>namespace Devlooped.Sponsors%3B

partial class SponsorLink
{
public partial class Funding
{
public const string PackageId = "$(FundingPackageId)"%3B
public const string Product = "$(FundingProduct)"%3B
public const string Prefix = "$(FundingPrefix)"%3B
public const int Grace = $(FundingGrace)%3B
Expand Down Expand Up @@ -141,7 +153,7 @@ partial class SponsorLink
<!--<ILRepackArgs>$(ILRepackArgs) "/lib:$(NetstandardDirectory)"</ILRepackArgs> -->
<!-- This is needed for ilrepack to find netstandard.dll, which is referenced by the System.Text.Json assembly -->
</PropertyGroup>
<Exec Command='"$(ILRepack)" $(ILRepackArgs)' WorkingDirectory="$(MSBuildProjectDirectory)\$(OutputPath)" StandardErrorImportance="high" IgnoreStandardErrorWarningFormat="true" StandardOutputImportance="low" ConsoleToMSBuild="true" ContinueOnError="true">
<Exec Command='ilrepack $(ILRepackArgs)' WorkingDirectory="$(MSBuildProjectDirectory)\$(OutputPath)" StandardErrorImportance="high" IgnoreStandardErrorWarningFormat="true" StandardOutputImportance="low" ConsoleToMSBuild="true" ContinueOnError="true">
<Output TaskParameter="ConsoleOutput" PropertyName="ILRepackOutput" />
<Output TaskParameter="ExitCode" PropertyName="ExitCode" />
</Exec>
Expand Down
120 changes: 80 additions & 40 deletions src/SponsorLink/SponsorLink/DiagnosticsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Humanizer;
using Humanizer.Localisation;
Expand Down Expand Up @@ -50,18 +51,19 @@ ConcurrentDictionary<string, Diagnostic> Diagnostics
/// <returns>The removed diagnostic, or <see langword="null" /> if none was previously pushed.</returns>
public void ReportOnce(Action<Diagnostic> report, string product = Funding.Product)
{
if (Diagnostics.TryRemove(product, out var diagnostic))
if (Diagnostics.TryRemove(product, out var diagnostic) &&
GetStatus(diagnostic) != SponsorStatus.Grace)
{
// Ensure only one such diagnostic is reported per product for the entire process,
// so that we can avoid polluting the error list with duplicates across multiple projects.
var id = string.Concat(Process.GetCurrentProcess().Id, product, diagnostic.Id);
using var mutex = new Mutex(false, "mutex" + id);
mutex.WaitOne();
using var mmf = MemoryMappedFile.CreateOrOpen(id, 1);
using var mmf = CreateOrOpenMemoryMappedFile(id, 1);
using var accessor = mmf.CreateViewAccessor();
if (accessor.ReadByte(0) == 0)
{
accessor.Write(0, 1);
accessor.Write(0, (byte)1);
report(diagnostic);
Tracing.Trace($"👈{diagnostic.Severity.ToString().ToLowerInvariant()}:{Process.GetCurrentProcess().Id}:{Process.GetCurrentProcess().ProcessName}:{product}:{diagnostic.Id}");
}
Expand All @@ -75,52 +77,61 @@ public void ReportOnce(Action<Diagnostic> report, string product = Funding.Produ
/// https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md under Ordering of actions).
/// </summary>
/// <returns>Optional <see cref="SponsorStatus"/> that was reported, if any.</returns>
/// <devdoc>
/// The SponsorLinkAnalyzer.GetOrSetStatus 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.
/// </devdoc>
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(Funding.Product, out var diagnostic) &&
diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value))
{
// Switch on value matching DiagnosticKind names
return value switch
{
nameof(SponsorStatus.Unknown) => SponsorStatus.Unknown,
nameof(SponsorStatus.Sponsor) => SponsorStatus.Sponsor,
nameof(SponsorStatus.Expiring) => SponsorStatus.Expiring,
nameof(SponsorStatus.Expired) => SponsorStatus.Expired,
_ => null,
};
}

return null;
}
=> Diagnostics.TryGetValue(Funding.Product, out var diagnostic) ? GetStatus(diagnostic) : null;

/// <summary>
/// Gets the status of the <see cref="Funding.Product"/>, or sets it from
/// the given set of <paramref name="manifests"/> if not already set.
/// </summary>
public SponsorStatus GetOrSetStatus(ImmutableArray<AdditionalText> manifests)
=> GetOrSetStatus(() => manifests);
public SponsorStatus GetOrSetStatus(ImmutableArray<AdditionalText> manifests, AnalyzerConfigOptionsProvider options)
=> GetOrSetStatus(() => manifests, () => options.GlobalOptions);

/// <summary>
/// Gets the status of the <see cref="Funding.Product"/>, or sets it from
/// the given analyzer <paramref name="options"/> if not already set.
/// </summary>
public SponsorStatus GetOrSetStatus(Func<AnalyzerOptions?> options)
=> GetOrSetStatus(() => options().GetSponsorManifests());
=> GetOrSetStatus(() => options().GetSponsorAdditionalFiles(), () => options()?.AnalyzerConfigOptionsProvider.GlobalOptions);

SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getManifests)
SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getAdditionalFiles, Func<AnalyzerConfigOptions?> getGlobalOptions)
{
if (GetStatus() is { } status)
return status;

if (!SponsorLink.TryRead(out var claims, getManifests().Select(text =>
if (!SponsorLink.TryRead(out var claims, getAdditionalFiles().Where(x => x.Path.EndsWith(".jwt")).Select(text =>
(text.GetText()?.ToString() ?? "", Sponsorables[Path.GetFileNameWithoutExtension(text.Path)]))) ||
claims.GetExpiration() is not DateTime exp)
{
var noGrace = getGlobalOptions() is { } globalOptions &&
globalOptions.TryGetValue("build_property.SponsorLinkNoInstallGrace", out var value) &&
bool.TryParse(value, out var skipCheck) && skipCheck;

if (noGrace != true)
{
// Consider grace period if we can find the install time.
var installed = getAdditionalFiles()
.Where(x => x.Path.EndsWith(".dll"))
.Select(x => File.GetLastWriteTime(x.Path))
.OrderByDescending(x => x)
.FirstOrDefault();

if (installed != default && ((DateTime.Now - installed).TotalDays <= Funding.Grace))
{
// report unknown, either unparsed manifest or one with no expiration (which we never emit).
Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Unknown], null,
properties: ImmutableDictionary.Create<string, string?>().Add(nameof(SponsorStatus), nameof(SponsorStatus.Grace)),
Funding.Product, Sponsorables.Keys.Humanize(Resources.Or)));
return SponsorStatus.Grace;
}
}

// report unknown, either unparsed manifest or one with no expiration (which we never emit).
Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Unknown], null,
properties: ImmutableDictionary.Create<string, string?>().Add(nameof(SponsorStatus), nameof(SponsorStatus.Unknown)),
Expand Down Expand Up @@ -169,25 +180,54 @@ Diagnostic Push(Diagnostic diagnostic, string product = Funding.Product)
var id = string.Concat(Process.GetCurrentProcess().Id, product, diagnostic.Id);
using var mutex = new Mutex(false, "mutex" + id);
mutex.WaitOne();
using var mmf = MemoryMappedFile.CreateOrOpen(id, 1);
using var mmf = CreateOrOpenMemoryMappedFile(id, 1);
using var accessor = mmf.CreateViewAccessor();
accessor.Write(0, 0);
accessor.Write(0, (byte)0);
Tracing.Trace($"👉{diagnostic.Severity.ToString().ToLowerInvariant()}:{Process.GetCurrentProcess().Id}:{Process.GetCurrentProcess().ProcessName}:{product}:{diagnostic.Id}");
}

return diagnostic;
}

SponsorStatus? GetStatus(Diagnostic? diagnostic) => diagnostic?.Properties.TryGetValue(nameof(SponsorStatus), out var value) == true
? value switch
{
nameof(SponsorStatus.Grace) => SponsorStatus.Grace,
nameof(SponsorStatus.Unknown) => SponsorStatus.Unknown,
nameof(SponsorStatus.Sponsor) => SponsorStatus.Sponsor,
nameof(SponsorStatus.Expiring) => SponsorStatus.Expiring,
nameof(SponsorStatus.Expired) => SponsorStatus.Expired,
_ => null,
}
: null;

static MemoryMappedFile CreateOrOpenMemoryMappedFile(string mapName, int capacity)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return MemoryMappedFile.CreateOrOpen(mapName, capacity);
}
else
{
// On Linux, use a file-based memory-mapped file
string filePath = $"/tmp/{mapName}";
using (var fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
fs.Write(new byte[capacity], 0, capacity);

return MemoryMappedFile.CreateFromFile(filePath, FileMode.OpenOrCreate);
}
}

internal static DiagnosticDescriptor CreateSponsor(string[] sponsorable, string prefix) => new(
$"{prefix}100",
Resources.Sponsor_Title,
Resources.Sponsor_Message,
"SponsorLink",
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: Resources.Sponsor_Description,
helpLinkUri: "https://github.com/devlooped#sponsorlink",
"DoesNotSupportF1Help");
$"{prefix}100",
Resources.Sponsor_Title,
Resources.Sponsor_Message,
"SponsorLink",
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: Resources.Sponsor_Description,
helpLinkUri: "https://github.com/devlooped#sponsorlink",
"DoesNotSupportF1Help");

internal static DiagnosticDescriptor CreateUnknown(string[] sponsorable, string product, string prefix) => new(
$"{prefix}101",
Expand Down
Loading

0 comments on commit d53f442

Please sign in to comment.