Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions PolySharp.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<Project Path="tests/PolySharp.PolySharpUseTypeAliasForUnmanagedCallersOnlyAttribute.Tests/PolySharp.PolySharpUseTypeAliasForUnmanagedCallersOnlyAttribute.Tests.csproj" />
<Project Path="tests/PolySharp.Tests/PolySharp.Tests.csproj" />
<Project Path="tests/PolySharp.TypeForwards.Tests/PolySharp.TypeForwards.Tests.csproj" />
<Project Path="tests/PolySharp.AlwaysGeneratePolyfills.Tests/PolySharp.AlwaysGeneratePolyfills.Tests.csproj" />
<Project Path="tests/PolySharp.TestLibraryA/PolySharp.TestLibraryA.csproj" />
<Project Path="tests/PolySharp.TestLibraryB/PolySharp.TestLibraryB.csproj" />
</Folder>
<Folder Name="/_SolutionItems/">
<File Path=".editorconfig" />
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
52 changes: 52 additions & 0 deletions src/PolySharp.Package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
<packageSources>
<add key="Temporary" value="C:\work\PolySharp\artifacts" />
</packageSources>
</configuration>
```
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.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ internal static class PolySharpMSBuildProperties
/// </summary>
public const string ExcludeGeneratedTypes = "PolySharpExcludeGeneratedTypes";

/// <summary>
/// The MSBuild property for <see cref="Models.GenerationOptions.AlwaysGeneratePolyfills"/>.
/// </summary>
public const string AlwaysGeneratePolyfills = "PolySharpAlwaysGeneratePolyfills";

/// <summary>
/// The MSBuild property for <see cref="Models.GenerationOptions.IncludeGeneratedTypes"/>.
/// </summary>
Expand Down
186 changes: 186 additions & 0 deletions src/PolySharp.SourceGenerators/DebugHelper.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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;
}

/// <summary>
/// Extracts the target framework from the compilation using TargetFrameworkAttribute.
/// </summary>
/// <param name="compilation">The compilation object.</param>
/// <returns>The target framework string, or null if not found.</returns>
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;
}

/// <summary>
/// Parses a full framework name into a target framework moniker.
/// </summary>
/// <param name="frameworkName">The full framework name from TargetFrameworkAttribute.</param>
/// <returns>The target framework moniker (e.g., "net472", "net6.0").</returns>
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ private static ImmutableArray<DiagnosticInfo> 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<string> excludeGeneratedTypes = options.GetStringArrayMSBuildProperty(PolySharpMSBuildProperties.ExcludeGeneratedTypes);

// Validate the fully qualified type names for "ExcludeGeneratedTypes"
Expand Down
2 changes: 2 additions & 0 deletions src/PolySharp.SourceGenerators/Models/GenerationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ namespace PolySharp.SourceGenerators.Models;
/// <param name="IncludeRuntimeSupportedAttributes">Whether to also generated dummy runtime supported attributes.</param>
/// <param name="UseInteropServices2NamespaceForUnmanagedCallersOnlyAttribute">Whether to move the <c>[UnmanagedCallersOnly]</c> type to a dummy <c>InteropServices2</c> namespace.</param>
/// <param name="ExcludeTypeForwardedToDeclarations">Whether to never generate any <c>[TypeForwardedTo]</c> declarations automatically.</param>
/// <param name="AlwaysGeneratePolyfills">Whether to generate polyfills even if they are available through a referenced project.</param>
/// <param name="ExcludeGeneratedTypes">The collection of fully qualified type names of types to exclude from generation.</param>
/// <param name="IncludeGeneratedTypes">The collection of fully qualified type names of types to include in the generation.</param>
internal sealed record GenerationOptions(
bool UsePublicAccessibilityForGeneratedTypes,
bool IncludeRuntimeSupportedAttributes,
bool UseInteropServices2NamespaceForUnmanagedCallersOnlyAttribute,
bool ExcludeTypeForwardedToDeclarations,
bool AlwaysGeneratePolyfills,
EquatableArray<string> ExcludeGeneratedTypes,
EquatableArray<string> IncludeGeneratedTypes);
1 change: 1 addition & 0 deletions src/PolySharp.SourceGenerators/PolySharp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
<CompilerVisibleProperty Include="PolySharpExcludeGeneratedTypes"/>
<CompilerVisibleProperty Include="PolySharpIncludeGeneratedTypes"/>
<CompilerVisibleProperty Include="PolySharpExcludeTypeForwardedToDeclarations"/>
<CompilerVisibleProperty Include="PolySharpAlwaysGeneratePolyfills"/>
</ItemGroup>

<!-- Adds necessary fixups for multiline properties (replaces ';' characters with ',' and strip new lines) -->
Expand Down
25 changes: 20 additions & 5 deletions src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,32 @@ 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<string> excludeGeneratedTypes = options.GetStringArrayMSBuildProperty(PolySharpMSBuildProperties.ExcludeGeneratedTypes);

// Gather the list of polyfills to explicitly include in the generation. This will override combinations expressed above.
ImmutableArray<string> 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);
}
Expand All @@ -105,9 +119,10 @@ private static GenerationOptions GetGenerationOptions(AnalyzerConfigOptionsProvi
/// Calculates the collection of <see cref="AvailableType"/> that could be generated.
/// </summary>
/// <param name="compilation">The current <see cref="Compilation"/> instance.</param>
/// <param name="options">The <see cref="GenerationOptions"/> for the current generation.</param>
/// <param name="token">The cancellation token for the operation.</param>
/// <returns>The collection of <see cref="AvailableType"/> that could be generated.</returns>
private static ImmutableArray<AvailableType> GetAvailableTypes(Compilation compilation, CancellationToken token)
private static ImmutableArray<AvailableType> 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))
Expand All @@ -116,12 +131,12 @@ private static ImmutableArray<AvailableType> 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;
}
Expand Down Expand Up @@ -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)));
}
Expand Down
5 changes: 4 additions & 1 deletion src/PolySharp.SourceGenerators/PolyfillsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public sealed partial class PolyfillsGenerator : IIncrementalGenerator
/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.SetupDebugging();

// Prepare all the generation options in a single incremental model
IncrementalValueProvider<GenerationOptions> generationOptions =
context.AnalyzerConfigOptionsProvider
Expand All @@ -21,7 +23,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// Get the sequence of all available types that could be generated
IncrementalValuesProvider<AvailableType> 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<GeneratedType> generatedTypes =
Expand Down
Loading