diff --git a/PolySharp.slnx b/PolySharp.slnx index ffda3b4..66c8b6c 100644 --- a/PolySharp.slnx +++ b/PolySharp.slnx @@ -10,6 +10,9 @@ + + + diff --git a/README.md b/README.md index 3e65b86..35b97fd 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,4 @@ The following properties are available: - "PolySharpExcludeGeneratedTypes": excludes specific types from generation (';' or ',' separated type names). - "PolySharpIncludeGeneratedTypes": only includes specific types for generation (';' or ',' separated type names). - "PolySharpExcludeTypeForwardedToDeclarations": never generates any `[TypeForwardedTo]` declarations. +- "PolySharpAlwaysGeneratePolyfills": generates the polyfills, even when they are available from the referenced projects. Addresses the issue https://github.com/Sergio0694/PolySharp/issues/50 \ No newline at end of file diff --git a/src/PolySharp.Package/README.md b/src/PolySharp.Package/README.md index 0cacf88..3d71282 100644 --- a/src/PolySharp.Package/README.md +++ b/src/PolySharp.Package/README.md @@ -94,3 +94,55 @@ The following properties are available: - "PolySharpExcludeGeneratedTypes": excludes specific types from generation (';' or ',' separated type names). - "PolySharpIncludeGeneratedTypes": only includes specific types for generation (';' or ',' separated type names). - "PolySharpExcludeTypeForwardedToDeclarations": never generates any `[TypeForwardedTo]` declarations. + +# Debugging + +If you suspect the generator does not work as expected, you can debug its operation relatively easily. Let us assume we want to debug the polyfills generation for a project producing assembly named `X`. + +## One time setup +1. Clone this repo, e.g. to **C:\work\PolySharp** +1. Add **C:\work\PolySharp\artifacts** to the package sources. E.g. you can use the nuget.config like this: + ``` + + + + + + + + + + ``` +1. Modify the version of the PolySharp package used by `X` to `1.0.0-alpha` +1. Install [DebugView](https://learn.microsoft.com/en-us/sysinternals/downloads/debugview) or any other trace log viewer. +1. Open a console window where debug iterations will take place. Let us assume it is pwsh and refer it as **the console**. +1. Run the trace viewer, filter messages containing **POLYSP** + +## Iteration + +On the console at **C:\work\PolySharp**: +1. Delete the NuGet package + ``` + del -r -force -EA SilentlyContinue ~\.nuget\packages\polysharp\1.0.0-alpha\,.\artifacts\,$env:TEMP\VBCSCompiler + ``` +1. Build the debug version of the package + ``` + dotnet pack -c:Debug .\src\PolySharp.Package\PolySharp.Package.msbuildproj + ``` +1. Request debug + ``` + $env:POLYSHARP_DEBUG = "AssemblyName[:TargetFramework]:DebugMode" + ``` + where: + - `AssemblyName` is the assembly name of the project in question, in our case `X` + - `TargetFramework` is optional - denotes the target framework of the project in question. Helpful when multiple target frameworks are built. + - `DebugMode` is one of the following: + - `1` or `launch` - results in a `Debugger.Launch()` call + - `2` or `attach` - results in an infinite loop, you are expected to attach the debugger and break out of the loop. The PID to attach to is in the trace. + - `3` or `trace` - currently just outputs the values of the build variables to the trace. +1. Build the project X only with the binary logs + ``` + msbuild /bl /v:m PATH_TO_X_CSPROJ /p:BuildProjectReferences=false + ``` + +The steps (1) and (2) are needed to get the debug version of the analyzer, which is easier to debug. They must be run each time you change the analyzer code or just once at the beginning. \ No newline at end of file diff --git a/src/PolySharp.SourceGenerators/Constants/PolySharpMSBuildProperties.cs b/src/PolySharp.SourceGenerators/Constants/PolySharpMSBuildProperties.cs index 13eb25c..83e7099 100644 --- a/src/PolySharp.SourceGenerators/Constants/PolySharpMSBuildProperties.cs +++ b/src/PolySharp.SourceGenerators/Constants/PolySharpMSBuildProperties.cs @@ -25,6 +25,11 @@ internal static class PolySharpMSBuildProperties /// public const string ExcludeGeneratedTypes = "PolySharpExcludeGeneratedTypes"; + /// + /// The MSBuild property for . + /// + public const string AlwaysGeneratePolyfills = "PolySharpAlwaysGeneratePolyfills"; + /// /// The MSBuild property for . /// diff --git a/src/PolySharp.SourceGenerators/DebugHelper.cs b/src/PolySharp.SourceGenerators/DebugHelper.cs new file mode 100644 index 0000000..234ae65 --- /dev/null +++ b/src/PolySharp.SourceGenerators/DebugHelper.cs @@ -0,0 +1,186 @@ +using Microsoft.CodeAnalysis; +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace PolySharp.SourceGenerators; + +internal static class DebugHelper +{ + internal static bool IsTraceEnabled { get; private set; } + internal static Action TraceWriteLine { get; private set; } = default!; + +#pragma warning disable IDE0044 // Add readonly modifier + private static bool s_debug = true; +#pragma warning restore IDE0044 // Add readonly modifier + + private enum DebugMode + { + None, + Launch, + Attach, + Trace, + } + + internal static void SetupDebugging(this IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput( + context.CompilationProvider, + static (context, compilation) => + { + switch (GetDebugMode(compilation, out string projectInfo)) + { + case DebugMode.Launch: + Trace.WriteLine($"[POLYSP] {projectInfo} - Launching debugger..."); + _ = Debugger.Launch(); + break; + case DebugMode.Attach: + Trace.WriteLine($"[POLYSP] {projectInfo} - Waiting for debugger attachment to PID {Process.GetCurrentProcess().Id} ..."); + while (s_debug) + { + Thread.Sleep(1000); + } + + break; + case DebugMode.Trace: + IsTraceEnabled = true; + TraceWriteLine = msg => Trace.WriteLine($"[POLYSP] {projectInfo} - {msg}"); + break; + } + }); + } + + private static DebugMode GetDebugMode(Compilation compilation, out string projectInfo) + { + string asmName = compilation.AssemblyName ?? "Unknown"; + string targetFramework = GetTargetFramework(compilation) ?? "unknown"; + projectInfo = $"{asmName} ({targetFramework})"; + + string? debug = Environment.GetEnvironmentVariable("POLYSHARP_DEBUG"); + Trace.WriteLine($"[POLYSP] {projectInfo} - Environment.GetEnvironmentVariable(\"POLYSHARP_DEBUG\") == {debug}"); + + if (string.IsNullOrEmpty(debug)) + { + return DebugMode.None; + } + + int i = debug.IndexOf(':'); + if (i < 0) + { + return DebugMode.None; + } + + string requestedAsmName = debug[..i]; + if (!requestedAsmName.Equals(asmName, StringComparison.OrdinalIgnoreCase)) + { + return DebugMode.None; + } + + ++i; + int j = debug.IndexOf(':', i); + if (j >= 0) + { + string requestedTargetFramework = debug[i..j]; + if (!requestedTargetFramework.Equals(targetFramework, StringComparison.OrdinalIgnoreCase)) + { + return DebugMode.None; + } + + i = j + 1; + } + + string mode = debug[i..]; + if (int.TryParse(mode, out int parsedMode)) + { + switch (parsedMode) + { + case 1: + return DebugMode.Launch; + case 2: + return DebugMode.Attach; + case 3: + return DebugMode.Trace; + default: + return DebugMode.None; + } + } + + if (Enum.TryParse(mode, true, out DebugMode parsedDebugMode)) + { + return parsedDebugMode; + } + + return DebugMode.None; + } + + /// + /// Extracts the target framework from the compilation using TargetFrameworkAttribute. + /// + /// The compilation object. + /// The target framework string, or null if not found. + private static string? GetTargetFramework(Compilation compilation) + { + AttributeData? targetFrameworkAttribute = compilation.Assembly + .GetAttributes() + .FirstOrDefault(attr => + attr.AttributeClass?.Name == "TargetFrameworkAttribute" && + attr.AttributeClass.ContainingNamespace?.ToDisplayString() == "System.Runtime.Versioning"); + + if (targetFrameworkAttribute?.ConstructorArguments.Length > 0) + { + string? frameworkName = targetFrameworkAttribute.ConstructorArguments[0].Value?.ToString(); + + // Parse framework name to extract just the TFM part + // Examples: ".NETFramework,Version=v4.7.2" -> "net472" + // ".NETCoreApp,Version=v6.0" -> "net6.0" + // ".NETStandard,Version=v2.0" -> "netstandard2.0" + if (!string.IsNullOrEmpty(frameworkName)) + { + return ParseTargetFrameworkMoniker(frameworkName); + } + } + + return null; + } + + /// + /// Parses a full framework name into a target framework moniker. + /// + /// The full framework name from TargetFrameworkAttribute. + /// The target framework moniker (e.g., "net472", "net6.0"). + private static string ParseTargetFrameworkMoniker(string? frameworkName) + { + if (string.IsNullOrEmpty(frameworkName)) + { + return "unknown"; + } + + // Handle common framework patterns + if (frameworkName!.StartsWith(".NETFramework,Version=v")) + { + string version = frameworkName.Substring(".NETFramework,Version=v".Length); + return version switch + { + "4.7.2" => "net472", + "4.8" => "net48", + "4.8.1" => "net481", + _ => $"net{version.Replace(".", "")}" + }; + } + + if (frameworkName.StartsWith(".NETCoreApp,Version=v")) + { + string version = frameworkName.Substring(".NETCoreApp,Version=v".Length); + return $"net{version}"; + } + + if (frameworkName.StartsWith(".NETStandard,Version=v")) + { + string version = frameworkName.Substring(".NETStandard,Version=v".Length); + return $"netstandard{version}"; + } + + return frameworkName; + } +} \ No newline at end of file diff --git a/src/PolySharp.SourceGenerators/Diagnostics/Analyzers/InvalidPolySharpMSBuildOptionAnalyzer.Execute.cs b/src/PolySharp.SourceGenerators/Diagnostics/Analyzers/InvalidPolySharpMSBuildOptionAnalyzer.Execute.cs index 7579020..8b64839 100644 --- a/src/PolySharp.SourceGenerators/Diagnostics/Analyzers/InvalidPolySharpMSBuildOptionAnalyzer.Execute.cs +++ b/src/PolySharp.SourceGenerators/Diagnostics/Analyzers/InvalidPolySharpMSBuildOptionAnalyzer.Execute.cs @@ -59,6 +59,14 @@ private static ImmutableArray GetOptionsDiagnostics(AnalyzerConf token.ThrowIfCancellationRequested(); + // And for "AlwaysGeneratePolyfills" as well + if (!options.IsValidMSBuildProperty(PolySharpMSBuildProperties.AlwaysGeneratePolyfills, out string? alwaysGeneratePolyfills)) + { + builder.Add(InvalidBoolMSBuildProperty, alwaysGeneratePolyfills, PolySharpMSBuildProperties.AlwaysGeneratePolyfills); + } + + token.ThrowIfCancellationRequested(); + ImmutableArray excludeGeneratedTypes = options.GetStringArrayMSBuildProperty(PolySharpMSBuildProperties.ExcludeGeneratedTypes); // Validate the fully qualified type names for "ExcludeGeneratedTypes" diff --git a/src/PolySharp.SourceGenerators/Models/GenerationOptions.cs b/src/PolySharp.SourceGenerators/Models/GenerationOptions.cs index f436b69..7919f5b 100644 --- a/src/PolySharp.SourceGenerators/Models/GenerationOptions.cs +++ b/src/PolySharp.SourceGenerators/Models/GenerationOptions.cs @@ -9,6 +9,7 @@ namespace PolySharp.SourceGenerators.Models; /// Whether to also generated dummy runtime supported attributes. /// Whether to move the [UnmanagedCallersOnly] type to a dummy InteropServices2 namespace. /// Whether to never generate any [TypeForwardedTo] declarations automatically. +/// Whether to generate polyfills even if they are available through a referenced project. /// The collection of fully qualified type names of types to exclude from generation. /// The collection of fully qualified type names of types to include in the generation. internal sealed record GenerationOptions( @@ -16,5 +17,6 @@ internal sealed record GenerationOptions( bool IncludeRuntimeSupportedAttributes, bool UseInteropServices2NamespaceForUnmanagedCallersOnlyAttribute, bool ExcludeTypeForwardedToDeclarations, + bool AlwaysGeneratePolyfills, EquatableArray ExcludeGeneratedTypes, EquatableArray IncludeGeneratedTypes); diff --git a/src/PolySharp.SourceGenerators/PolySharp.targets b/src/PolySharp.SourceGenerators/PolySharp.targets index e9f4075..deef88f 100644 --- a/src/PolySharp.SourceGenerators/PolySharp.targets +++ b/src/PolySharp.SourceGenerators/PolySharp.targets @@ -114,6 +114,7 @@ + diff --git a/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs b/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs index eec140e..b847342 100644 --- a/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs +++ b/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs @@ -85,6 +85,8 @@ private static GenerationOptions GetGenerationOptions(AnalyzerConfigOptionsProvi bool useInteropServices2NamespaceForUnmanagedCallersOnlyAttribute = options.GetBoolMSBuildProperty(PolySharpMSBuildProperties.UseInteropServices2NamespaceForUnmanagedCallersOnlyAttribute); bool excludeTypeForwardedToDeclarations = options.GetBoolMSBuildProperty(PolySharpMSBuildProperties.ExcludeTypeForwardedToDeclarations); + bool alwaysGeneratePolyfills = options.GetBoolMSBuildProperty(PolySharpMSBuildProperties.AlwaysGeneratePolyfills); + // Gather the list of any polyfills to exclude from generation (this can help to avoid conflicts with other generators). That's because // generators see the same compilation and can't know what others will generate, so $(PolySharpExcludeGeneratedTypes) can solve this issue. ImmutableArray excludeGeneratedTypes = options.GetStringArrayMSBuildProperty(PolySharpMSBuildProperties.ExcludeGeneratedTypes); @@ -92,11 +94,23 @@ private static GenerationOptions GetGenerationOptions(AnalyzerConfigOptionsProvi // Gather the list of polyfills to explicitly include in the generation. This will override combinations expressed above. ImmutableArray includeGeneratedTypes = options.GetStringArrayMSBuildProperty(PolySharpMSBuildProperties.IncludeGeneratedTypes); + if (DebugHelper.IsTraceEnabled) + { + DebugHelper.TraceWriteLine($"{PolySharpMSBuildProperties.UsePublicAccessibilityForGeneratedTypes} = {usePublicAccessibilityForGeneratedTypes}"); + DebugHelper.TraceWriteLine($"{PolySharpMSBuildProperties.IncludeRuntimeSupportedAttributes} = {includeRuntimeSupportedAttributes}"); + DebugHelper.TraceWriteLine($"{PolySharpMSBuildProperties.UseInteropServices2NamespaceForUnmanagedCallersOnlyAttribute} = {useInteropServices2NamespaceForUnmanagedCallersOnlyAttribute}"); + DebugHelper.TraceWriteLine($"{PolySharpMSBuildProperties.ExcludeTypeForwardedToDeclarations} = {excludeTypeForwardedToDeclarations}"); + DebugHelper.TraceWriteLine($"{PolySharpMSBuildProperties.AlwaysGeneratePolyfills} = {alwaysGeneratePolyfills}"); + DebugHelper.TraceWriteLine($"{PolySharpMSBuildProperties.ExcludeGeneratedTypes} = {string.Join(" , ", excludeGeneratedTypes)}"); + DebugHelper.TraceWriteLine($"{PolySharpMSBuildProperties.IncludeGeneratedTypes} = {string.Join(" , ", includeGeneratedTypes)}"); + } + return new( usePublicAccessibilityForGeneratedTypes, includeRuntimeSupportedAttributes, useInteropServices2NamespaceForUnmanagedCallersOnlyAttribute, excludeTypeForwardedToDeclarations, + alwaysGeneratePolyfills, excludeGeneratedTypes, includeGeneratedTypes); } @@ -105,9 +119,10 @@ private static GenerationOptions GetGenerationOptions(AnalyzerConfigOptionsProvi /// Calculates the collection of that could be generated. /// /// The current instance. + /// The for the current generation. /// The cancellation token for the operation. /// The collection of that could be generated. - private static ImmutableArray GetAvailableTypes(Compilation compilation, CancellationToken token) + private static ImmutableArray GetAvailableTypes(Compilation compilation, GenerationOptions options, CancellationToken token) { // A minimum of C# 8.0 is required to benefit from the polyfills if (!compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) @@ -116,12 +131,12 @@ private static ImmutableArray GetAvailableTypes(Compilation compi } // Helper function to check whether a type is available - static bool IsTypeAvailable(Compilation compilation, string name, CancellationToken token) + static bool IsTypeAvailable(Compilation compilation, string name, GenerationOptions options, CancellationToken token) { token.ThrowIfCancellationRequested(); - // First check whether the type is accessible, and if it is already then there is nothing left to do - if (compilation.HasAccessibleTypeWithMetadataName(name)) + // If AlwaysGeneratePolyfills is enabled, generate polyfills regardless of availability + if (!options.AlwaysGeneratePolyfills && compilation.HasAccessibleTypeWithMetadataName(name)) { return false; } @@ -169,7 +184,7 @@ static SyntaxFixupType GetSyntaxFixupType(Compilation compilation, string name) // Inspect all available types and filter them down according to the current compilation foreach (string name in AllSupportTypeNames) { - if (IsTypeAvailable(compilation, name, token)) + if (IsTypeAvailable(compilation, name, options, token)) { builder.Add(new AvailableType(name, GetSyntaxFixupType(compilation, name))); } diff --git a/src/PolySharp.SourceGenerators/PolyfillsGenerator.cs b/src/PolySharp.SourceGenerators/PolyfillsGenerator.cs index aff08fd..b92805a 100644 --- a/src/PolySharp.SourceGenerators/PolyfillsGenerator.cs +++ b/src/PolySharp.SourceGenerators/PolyfillsGenerator.cs @@ -13,6 +13,8 @@ public sealed partial class PolyfillsGenerator : IIncrementalGenerator /// public void Initialize(IncrementalGeneratorInitializationContext context) { + context.SetupDebugging(); + // Prepare all the generation options in a single incremental model IncrementalValueProvider generationOptions = context.AnalyzerConfigOptionsProvider @@ -21,7 +23,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Get the sequence of all available types that could be generated IncrementalValuesProvider availableTypes = context.CompilationProvider - .SelectMany(GetAvailableTypes); + .Combine(generationOptions) + .SelectMany(static (pair, token) => GetAvailableTypes(pair.Left, pair.Right, token)); // Gather the sequence of all types to generate after filtering IncrementalValuesProvider generatedTypes = diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..1263378 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,8 @@ + + + $(NoWarn);CS1591 + false + $([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)\..\')) + + + \ No newline at end of file diff --git a/tests/PolySharp.AlwaysGeneratePolyfills.Tests/AlwaysGeneratePolyfillsTests.cs b/tests/PolySharp.AlwaysGeneratePolyfills.Tests/AlwaysGeneratePolyfillsTests.cs new file mode 100644 index 0000000..2a9f0a3 --- /dev/null +++ b/tests/PolySharp.AlwaysGeneratePolyfills.Tests/AlwaysGeneratePolyfillsTests.cs @@ -0,0 +1,13 @@ +using PolySharp.TestLibraryA; +using PolySharp.TestLibraryB; +using System; + +namespace PolySharp.AlwaysGeneratePolyfills.Tests; + +public class ConsumerClass +{ + public Type TestLibraryA => typeof(LibraryAClass); + public Type TestLibraryB => typeof(LibraryBClass); + public required string RequiredValue { get; init; } + public string? OptionalValue { get; init; } +} diff --git a/tests/PolySharp.AlwaysGeneratePolyfills.Tests/PolySharp.AlwaysGeneratePolyfills.Tests.csproj b/tests/PolySharp.AlwaysGeneratePolyfills.Tests/PolySharp.AlwaysGeneratePolyfills.Tests.csproj new file mode 100644 index 0000000..49dc45f --- /dev/null +++ b/tests/PolySharp.AlwaysGeneratePolyfills.Tests/PolySharp.AlwaysGeneratePolyfills.Tests.csproj @@ -0,0 +1,17 @@ + + + + net472 + true + + + + + + + + + + + + diff --git a/tests/PolySharp.TestLibraryA/LibraryAClass.cs b/tests/PolySharp.TestLibraryA/LibraryAClass.cs new file mode 100644 index 0000000..caf48bd --- /dev/null +++ b/tests/PolySharp.TestLibraryA/LibraryAClass.cs @@ -0,0 +1,6 @@ +namespace PolySharp.TestLibraryA; + +internal class LibraryAClass +{ + public required string Name { get; init; } +} diff --git a/tests/PolySharp.TestLibraryA/PolySharp.TestLibraryA.csproj b/tests/PolySharp.TestLibraryA/PolySharp.TestLibraryA.csproj new file mode 100644 index 0000000..4ad33b1 --- /dev/null +++ b/tests/PolySharp.TestLibraryA/PolySharp.TestLibraryA.csproj @@ -0,0 +1,12 @@ + + + + net472 + + + + + + + + diff --git a/tests/PolySharp.TestLibraryB/LibraryBClass.cs b/tests/PolySharp.TestLibraryB/LibraryBClass.cs new file mode 100644 index 0000000..8d3e36c --- /dev/null +++ b/tests/PolySharp.TestLibraryB/LibraryBClass.cs @@ -0,0 +1,6 @@ +namespace PolySharp.TestLibraryB; + +internal class LibraryBClass +{ + public required string Title { get; init; } +} diff --git a/tests/PolySharp.TestLibraryB/PolySharp.TestLibraryB.csproj b/tests/PolySharp.TestLibraryB/PolySharp.TestLibraryB.csproj new file mode 100644 index 0000000..4ad33b1 --- /dev/null +++ b/tests/PolySharp.TestLibraryB/PolySharp.TestLibraryB.csproj @@ -0,0 +1,12 @@ + + + + net472 + + + + + + + +