Skip to content
Merged
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
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"allowPrerelease": false
},
"msbuild-sdks": {
"Purview.DotNetProjectSdk": "1.0.0-prerelease.45"
"Purview.DotNetProjectSdk": "1.0.0-prerelease.46"
},
"test": {
"runner": "Microsoft.Testing.Platform"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "purview-sourcegeneratorframework",
"version": "1.0.0-prerelease.29",
"version": "1.0.0-prerelease.30",
"private": true
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsRoslynComponent>true</IsRoslynComponent>
<AssemblyName>$(RootNamespace)</AssemblyName>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\SourceGeneratorShared\SourceGeneratorShared.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>

<Target
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsRoslynComponent>true</IsRoslynComponent>
<AssemblyName>$(RootNamespace)</AssemblyName>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\SourceGeneratorShared\SourceGeneratorShared.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>

<Target
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent>
<NoWarn>$(NoWarn);IDE0130;</NoWarn>
</PropertyGroup>
Expand Down Expand Up @@ -31,7 +30,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsRoslynComponent>true</IsRoslynComponent>
<AssemblyName>$(RootNamespace)</AssemblyName>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\SourceGeneratorShared\SourceGeneratorShared.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>

<Target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,10 @@ public SourceGeneratorTestOptions()

/// <summary>
/// Gets the nullable context of the test compilation. The default is
/// <see cref="NullableContextOptions.Disable"/>, mirroring the framework's auto-detection of the
/// <c>#nullable enable</c> directive in generated headers.
/// <see cref="NullableContextOptions.Enable"/>, so test compilations accept nullable annotations
/// and the framework's auto-detection emits the <c>#nullable enable</c> directive in generated headers.
/// </summary>
public NullableContextOptions NullableContextOptions { get; init; } = NullableContextOptions.Disable;
public NullableContextOptions NullableContextOptions { get; init; } = NullableContextOptions.Enable;

/// <summary>
/// Gets the language version of the test compilation.
Expand Down
12 changes: 8 additions & 4 deletions src/src/SourceGeneratorFramework/Sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ See [`SourceGeneratorFramework.ExampleGenerator`](../SourceGeneratorFramework.Ex

### The `#nullable enable` directive

`WriteAutoGeneratedHeader()` emits `#nullable enable` according to a `NullableDirectiveMode`:
`WriteAutoGeneratedHeader()` emits `#nullable enable` according to a `NullableDirectiveMode`. The same mode also controls whether nullable *reference* annotations are rendered by type writing, so the directive and the emitted annotations always agree:

- `Auto` (default) — the framework reads the target compilation's nullable context when the pipeline creates the generation context and emits the directive only when nullable annotations are enabled. When the state is unknown (for example in post-initialization outputs or tests), the directive is still emitted.
- `Always` — always emit `#nullable enable`.
- `Disable` — never emit the directive.
- `Auto` (default) — the framework reads the target compilation's nullable context when the pipeline creates the generation context and emits the directive only when nullable annotations are enabled. When the state is unknown (for example in post-initialization outputs or tests), the directive is still emitted. An explicitly configured `GenerationSettings.IsNullableContextEnabled` value takes precedence over the compilation's state.
- `Always` — always emit `#nullable enable` and always render nullable reference annotations, even when the target compilation disables nullable.
- `Disable` — never emit the directive and never render nullable reference annotations.

Override it per call, or set a generator-wide default on `GenerationSettings`:

Expand All @@ -181,6 +181,8 @@ var settings = GenerationSettings.Create<MyGenerator>() with
};
```

If your generated output must use nullable reference annotations regardless of the consuming project's `<Nullable>` setting, configure `NullableDirectiveMode.Always` (or set `IsNullableContextEnabled = true`) on the `GenerationSettings` passed into the pipeline; the configured value is honoured instead of the compilation's state.

### Nullable reference annotations in generated types

`TypeReference.Nullable()` and `TypeIdentity.MakeNullable()` produce nullable annotations. When the target compilation does not support nullable, a nullable *reference* annotation (`string?`) is invalid outside a nullable context and is elided, while a nullable *value* type (`int?`) is always emitted.
Expand All @@ -199,6 +201,8 @@ writer.WriteType(PurviewTypeLibrary.System.String.MakeNullable(writer)); // elid
writer.WriteType(TypeIdentity.Create<string>().MakeNullable()); // "string" when nullable is off, "string?" when on
```

Composition and rendering both resolve `NullableDirectiveMode` together with `IsNullableContextEnabled`, so an `Always` mode keeps the `?` even for a nullable-disabled target, and a `Disable` mode strips it even when the target enables nullable.

The analyzer `PSGFR16` (suggestion) flags bare `Nullable()`/`MakeNullable()` calls and its code fix passes the first in-scope `CodeWriter` or `GenerationSettings`, including project-wide "Fix all" support.

### Comparing references with or without annotations
Expand Down
47 changes: 6 additions & 41 deletions src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
<IncludeBuildOutput>true</IncludeBuildOutput>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);IncludeSourceGeneratorShared;IncludeAnalyzerAssembly</TargetsForTfmSpecificContentInPackage>
<!-- The repo's pack validation requires PDBs to be delivered only through the .snupkg, so the
analyzer PDB must not ship inside the .nupkg under analyzers/dotnet/cs. -->
<PurviewPackAnalyzerPdb>false</PurviewPackAnalyzerPdb>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);IncludeSourceGeneratorShared</TargetsForTfmSpecificContentInPackage>
<TargetsForTfmSpecificDebugSymbolsInPackage>$(TargetsForTfmSpecificDebugSymbolsInPackage);IncludeSourceGeneratorSharedSymbols</TargetsForTfmSpecificDebugSymbolsInPackage>
</PropertyGroup>

Expand All @@ -22,15 +25,12 @@
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
Targets="GetSourceGeneratorAnalyzerFiles"
/>
<ProjectReference
Include="..\SourceGeneratorFramework.Analyzers\SourceGeneratorFramework.Analyzers.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
Targets="GetSourceGeneratorAnalyzerFiles"
Pack="false"
/>
<ProjectReference
Include="..\SourceGeneratorShared\SourceGeneratorShared.csproj"
Expand All @@ -40,44 +40,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>

<Import Project="..\SourceGeneratorShared\SourceGeneratorShared.Pack.targets" />

<!-- The SDK auto-packs the built analyzer assembly's PDB into analyzers/dotnet/cs as package
content, which would put a PDB in the .nupkg. The PDB is already collected for the .snupkg
via DebugSymbolsProjectOutputGroup, so remove the content item before the pack file set is built. -->
<Target Name="RemoveSdkAnalyzerPdbFromPackage" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<None Remove="$(TargetDir)$(TargetName).pdb" />
</ItemGroup>
</Target>

<!--
By setting the Analyzer reference to Pack="false", we prevent it from being included in the NuGet package.
This solved the warning about duplicate .Shared.dll files being included in the package, but it also means that the analyzer
assembly is not included in the package at all.

So this target is added to explicitly include the analyzer assembly in the package, so that it can be used by other projects that reference this package.
-->
<Target Name="IncludeAnalyzerAssembly">
<MSBuild
Projects="..\SourceGeneratorFramework.Analyzers\SourceGeneratorFramework.Analyzers.csproj"
Targets="GetTargetPath"
Properties="Configuration=$(Configuration);TargetFramework=netstandard2.0"
>
<Output TaskParameter="TargetOutputs" ItemName="_SourceGeneratorFrameworkAnalyzerAssembly" />
</MSBuild>

<ItemGroup>
<TfmSpecificPackageFile
Include="@(_SourceGeneratorFrameworkAnalyzerAssembly)"
Condition="Exists('%(_SourceGeneratorFrameworkAnalyzerAssembly.Identity)')"
>
<PackagePath>analyzers/dotnet/cs/</PackagePath>
</TfmSpecificPackageFile>
</ItemGroup>
</Target>
</Project>
37 changes: 25 additions & 12 deletions src/src/SourceGeneratorShared/CodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ public CodeWriter(
public string GeneratorVersion { get; }

/// <summary>
/// Gets or sets how the <c>#nullable enable</c> directive is emitted by
/// <see cref="WriteAutoGeneratedHeader"/>. The value is seeded from
/// Gets or sets how nullable annotations and the <c>#nullable enable</c> directive are emitted by
/// <see cref="WriteAutoGeneratedHeader"/> and type rendering. The value is seeded from
/// <see cref="GenerationSettings.NullableDirectiveMode"/> at construction.
/// </summary>
public NullableDirectiveMode NullableDirectiveMode { get; set; }
Expand All @@ -113,6 +113,11 @@ public CodeWriter(
/// seeded from <see cref="GenerationSettings.IsNullableContextEnabled"/> at construction and is
/// <see langword="null"/> when the state is unknown.
/// </summary>
/// <remarks>
/// When <see cref="NullableDirectiveMode"/> is <see cref="NullableDirectiveMode.Always"/> or
/// <see cref="NullableDirectiveMode.Disable"/>, the mode controls both the header directive and
/// rendered annotations; this value only drives the <see cref="NullableDirectiveMode.Auto"/> mode.
/// </remarks>
public bool? IsNullableContextEnabled { get; set; }

/// <summary>
Expand Down Expand Up @@ -1337,9 +1342,9 @@ public CodeWriter WriteConstructor(ConstructorDeclarationOptions declaration, Ac
/// <param name="generatorName">The generator name; defaults to <see cref="GeneratorName"/>.</param>
/// <param name="version">The generator version; defaults to <see cref="GeneratorVersion"/>.</param>
/// <param name="nullableDirective">
/// Controls whether the <c>#nullable enable</c> directive is emitted. When <see langword="null"/>,
/// <see cref="NullableDirectiveMode"/> is used, which defaults to
/// <see cref="NullableDirectiveMode.Auto"/>.
/// Controls whether the <c>#nullable enable</c> directive is emitted and whether nullable
/// reference annotations are rendered. When <see langword="null"/>, <see cref="NullableDirectiveMode"/>
/// is used, which defaults to <see cref="NullableDirectiveMode.Auto"/>.
/// </param>
/// <param name="pragmas">The pragmas to include in the header.</param>
/// <returns>The current writer.</returns>
Expand Down Expand Up @@ -1391,10 +1396,17 @@ static bool ShouldWriteNullableDirective(NullableDirectiveMode mode, bool? isNul
{
NullableDirectiveMode.Always => true,
NullableDirectiveMode.Disable => false,
NullableDirectiveMode.Auto => isNullableContextEnabled ?? true,
NullableDirectiveMode.Auto => isNullableContextEnabled is null or true,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unknown nullable directive mode."),
};

/// <summary>
/// Resolves whether nullable reference annotations are emitted by this writer, reconciling
/// <see cref="NullableDirectiveMode"/> and <see cref="IsNullableContextEnabled"/> so that the
/// header directive and type rendering always agree.
/// </summary>
bool ShouldUseNullableAnnotations => ShouldWriteNullableDirective(NullableDirectiveMode, IsNullableContextEnabled);

/// <summary>
/// Writes a <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> declaration.
/// </summary>
Expand Down Expand Up @@ -2813,9 +2825,10 @@ string parameterName
/// <param name="reference">The type reference to write.</param>
/// <returns>The current writer.</returns>
/// <remarks>
/// When the writer's <see cref="IsNullableContextEnabled"/> is <see langword="false"/>, nullable reference
/// annotations such as <c>string?</c> are elided because they are invalid outside a nullable context.
/// Nullable value types such as <c>int?</c> are always written.
/// When the writer's nullable context resolves to disabled (see <see cref="NullableDirectiveMode"/>
/// and <see cref="IsNullableContextEnabled"/>), nullable reference annotations such as
/// <c>string?</c> are elided because they are invalid outside a nullable context. Nullable value
/// types such as <c>int?</c> are always written.
/// </remarks>
public CodeWriter WriteType(TypeReference reference)
{
Expand All @@ -2826,7 +2839,7 @@ public CodeWriter WriteType(TypeReference reference)

ValidateTypeReference(reference, nameof(reference));

return Write(reference.RenderFullNameForNullable(IsNullableContextEnabled is not false));
return Write(reference.RenderFullNameForNullable(ShouldUseNullableAnnotations));
}

CodeWriter WriteTypeReference(TypeReference reference)
Expand All @@ -2836,13 +2849,13 @@ CodeWriter WriteTypeReference(TypeReference reference)

ValidateTypeReference(reference, nameof(reference));

Write(reference.RenderFullNameForNullable(IsNullableContextEnabled is not false));
Write(reference.RenderFullNameForNullable(ShouldUseNullableAnnotations));

return this;
}

int GetTypeReferenceLength(TypeReference type) =>
type.IsEmpty ? 0 : type.RenderFullNameForNullable(IsNullableContextEnabled is not false).Length;
type.IsEmpty ? 0 : type.RenderFullNameForNullable(ShouldUseNullableAnnotations).Length;

static void ValidateTypeReference(TypeReference reference, string parameterName)
{
Expand Down
9 changes: 5 additions & 4 deletions src/src/SourceGeneratorShared/GenerationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ public GenerationSettings(
public string? DisabledSourceGenMSBuildProperty { get; }

/// <summary>
/// Gets how the <c>#nullable enable</c> directive is emitted by generated headers written via
/// <c>CodeWriter.WriteAutoGeneratedHeader</c>. The default is <see cref="NullableDirectiveMode.Auto"/>,
/// Gets how nullable annotations and the <c>#nullable enable</c> directive are emitted by generated
/// code written via <c>CodeWriter</c>. The default is <see cref="NullableDirectiveMode.Auto"/>,
/// which uses <see cref="IsNullableContextEnabled"/> when it is known.
/// </summary>
public NullableDirectiveMode NullableDirectiveMode { get; init; } = NullableDirectiveMode.Auto;

/// <summary>
/// Gets whether the target compilation has nullable annotations enabled. The incremental pipeline
/// sets this when the compilation is available; <see langword="null"/> when the value is unknown,
/// such as for post-initialization outputs or tests that construct settings directly.
/// sets this from the compilation when it is available and no explicit value was configured;
/// <see langword="null"/> when the value is unknown, such as for post-initialization outputs or
/// tests that construct settings directly.
/// </summary>
public bool? IsNullableContextEnabled { get; init; }

Expand Down
30 changes: 17 additions & 13 deletions src/src/SourceGeneratorShared/Helpers/IncrementalPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,20 @@ public static IncrementalValueProvider<
$"Creating generation context ({typeof(TCapabilities)}) for compilation '{input.Left.AssemblyName}'."
);

settings = settings with
// Honour an explicitly configured nullable-context value first, falling back to the
// compilation's nullable annotations state only when one was not specified.
var resolvedSettings = settings with
{
ValidateCodeWriterScopes = configuration.ValidateCodeWriterScopes,
IsSourceGeneratorDisabled = configuration.IsSourceGeneratorDisabled,
IsLoggingEnabled = logger is not null,
IsNullableContextEnabled = IsNullableContextEnabled(compilation),
IsNullableContextEnabled =
settings.IsNullableContextEnabled ?? IsNullableContextEnabled(compilation),
};

var capabilities = factory(compilation, settings, logger, cancellationToken);
var capabilities = factory(compilation, resolvedSettings, logger, cancellationToken);

return new GenerationContext<TCapabilities>(capabilities, settings, logger);
return new GenerationContext<TCapabilities>(capabilities, resolvedSettings, logger);
}
)
.WithTrackingName($"GetGenerationContext_{typeof(TCapabilities).Name}");
Expand All @@ -117,16 +120,17 @@ public static IncrementalValueProvider<
/// <summary>
/// Determines whether the compilation has nullable annotations enabled, which is the case when
/// the compilation options allow nullable annotations. Returns <see langword="null"/> when the
/// compilation is not a C# compilation.
/// compilation is not a C# compilation or the state cannot be determined.
/// </summary>
#pragma warning disable format
static bool? IsNullableContextEnabled(Compilation compilation) =>
compilation
is CSharpCompilation
{
Options.NullableContextOptions: NullableContextOptions.Annotations or NullableContextOptions.Enable
};
#pragma warning restore format
public static bool? IsNullableContextEnabled(Compilation compilation)
{
if (compilation is not CSharpCompilation csharpCompilation)
return null;

return csharpCompilation.Options.NullableContextOptions
is NullableContextOptions.Annotations
or NullableContextOptions.Enable;
}

static IncrementalValueProvider<GenerationConfiguration> GenerationConfigurationValueProvider(
IncrementalGeneratorInitializationContext context,
Expand Down
Loading