diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 6e7d513..a0969d9 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -11,21 +11,9 @@ concurrency:
jobs:
build:
name: Build and test
- runs-on: ubuntu-latest
- timeout-minutes: 30
- steps:
- - uses: actions/checkout@v7
- with:
- fetch-depth: 0
- fetch-tags: true
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v6
- with:
- dotnet-version: "10.0.x"
-
- - name: Run PR pipeline
- env:
- Build__RunPack: "true"
- Build__ValidatePack: "true"
- run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release
+ uses: purview-dev/build/.github/workflows/purview-build.yml@main
+ with:
+ build-version: "0.2.1"
+ run-pack: true
+ validate-pack: true
+ secrets: inherit
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e8706da..97abb5f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,41 +11,9 @@ concurrency:
jobs:
release:
name: Release packages
- runs-on: ubuntu-latest
- timeout-minutes: 30
- permissions:
- contents: write
- steps:
- - uses: actions/checkout@v7
- with:
- fetch-depth: 0
- fetch-tags: true
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v6
- with:
- dotnet-version: "10.0.x"
-
- - name: Check for version bump
- id: version
- shell: bash
- run: |
- VERSION=$(node -p "require('./package.json').version")
- TAG="v$VERSION"
- if git rev-parse "$TAG" >/dev/null 2>&1; then
- echo "Version $VERSION is already tagged as $TAG. Skipping release."
- echo "should_publish=false" >> "$GITHUB_OUTPUT"
- else
- echo "New version $VERSION detected. Releasing $TAG."
- echo "should_publish=true" >> "$GITHUB_OUTPUT"
- echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Run release pipeline
- if: steps.version.outputs.should_publish == 'true'
- env:
- Release__Mode: NuGet
- NuGet__ApiKey: ${{ secrets.NUGET__APIKEY }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release
+ uses: purview-dev/build/.github/workflows/purview-release.yml@main
+ with:
+ build-version: "0.2.1"
+ release-mode: NuGet
+ release-branch: main
+ secrets: inherit
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 1baaa36..a047b9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -660,3 +660,5 @@ sketch
BenchmarkDotNet.Artifacts/
!**/Sdk/build
!build/
+
+.tools/
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 23c42bb..c67f4ae 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,15 +4,8 @@
[4.13.0,)
[5.9.0,)
[1.65.51,)
- [3.2.8,)
-
-
-
-
-
-
diff --git a/Justfile b/Justfile
index fb6706f..3272540 100644
--- a/Justfile
+++ b/Justfile
@@ -5,8 +5,9 @@ solution := root_folder / "SourceGeneratorFramework.slnx"
build_configuration := "Release"
artifacts_folder := "./artifacts"
default_test_filter := "/*/*/*/*/"
-pipeline_solution := "build/Pipeline.slnx"
-pipeline_project := "build/PipelineCLI/PipelineCLI.csproj"
+pipeline_version := "0.2.1"
+pipeline_feed := "https://api.nuget.org/v3/index.json"
+pipeline_tool := ".tools/purview-build/purview-build"
current_version := `node -p "require('./package.json').version"`
@@ -14,38 +15,50 @@ current_version := `node -p "require('./package.json').version"`
default:
just --list
+# Install the shared Purview.Build tool (authenticated to the Purview-Dev feed) if not present
+[private]
+ensure-pipeline-tool:
+ if [ ! -x "{{ pipeline_tool }}" ]; then \
+ dotnet tool install Purview.Build --tool-path .tools/purview-build --add-source "{{ pipeline_feed }}" --version "{{ pipeline_version }}"; \
+ fi
+
# Run the PR pipeline (restore, build, lint, tests)
[group('Pipeline')]
pipeline-pr *args:
+ just ensure-pipeline-tool
echo "Running PR pipeline..."
- dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} {{ args }}
+ "{{ pipeline_tool }}" {{ args }}
# Run the build pipeline (restore, build, lint)
[group('Pipeline')]
pipeline-build *args:
+ just ensure-pipeline-tool
echo "Running build pipeline..."
- dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Build:RunTests=false --Release:Mode=None {{ args }}
+ "{{ pipeline_tool }}" --Build:RunTests=false --Release:Mode=None {{ args }}
# Run the release pipeline (restore, build, lint, tests, pack, publish, GitHub release)
[group('Pipeline')]
pipeline-release *args:
+ just ensure-pipeline-tool
echo "Running release pipeline..."
- dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Release:Mode=NuGet {{ args }}
+ "{{ pipeline_tool }}" --Release:Mode=NuGet {{ args }}
# Run the release pipeline (restore, build, lint, tests, pack, local nuget publish)
# Note: `just` runs recipes through the shell, which strips backslashes from unquoted arguments.
-# Always use forward slashes for the feed path, e.g.
+# Use the LOCAL_NUGET_FEED_PATH environment variable or forward slashes, e.g.
# just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/
[group('Pipeline')]
pipeline-local-release *args:
+ just ensure-pipeline-tool
echo "Running local release pipeline..."
- dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Release:Mode=LocalNuGet {{ args }}
+ "{{ pipeline_tool }}" --Release:Mode=LocalNuGet {{ args }}
# Run the pipeline with tests enabled
[group('Pipeline')]
pipeline-tests *args:
+ just ensure-pipeline-tool
echo "Running tests pipeline..."
- dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Build:RunTests=true --Release:Mode=None {{ args }}
+ "{{ pipeline_tool }}" --Build:RunTests=true --Release:Mode=None {{ args }}
# Build and test with the specified configuration, defaulting to "Release"
[group('Build and Test')]
@@ -105,8 +118,3 @@ lint-fix:
[group('Utilities')]
vs:
open {{ solution }}
-
-# Open the solution in Visual Studio/ Registered application
-[group('Utilities')]
-vs-pipeline:
- open {{ pipeline_solution }}
diff --git a/build/Directory.Build.props b/build/Directory.Build.props
deleted file mode 100644
index 772d7f7..0000000
--- a/build/Directory.Build.props
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- Purview.SourceGeneratorFramework
- true
-
-
-
-
-
- $(NoWarn);CA1062;CA1515;CA2007;CA1873;
-
-
diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets
deleted file mode 100644
index a3bbd31..0000000
--- a/build/Directory.Build.targets
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/build/Pipeline.slnx b/build/Pipeline.slnx
deleted file mode 100644
index 410fbba..0000000
--- a/build/Pipeline.slnx
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/build/PipelineCLI/GlobalUsings.cs b/build/PipelineCLI/GlobalUsings.cs
deleted file mode 100644
index ba849f1..0000000
--- a/build/PipelineCLI/GlobalUsings.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-global using Microsoft.Extensions.Configuration;
-global using Microsoft.Extensions.DependencyInjection;
-global using Microsoft.Extensions.Logging;
-global using Microsoft.Extensions.Options;
-global using ModularPipelines;
-global using ModularPipelines.Extensions;
-global using Octokit;
-global using Octokit.Internal;
-global using Purview.SourceGeneratorFramework.PipelineCLI.Helpers;
-global using Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-global using Purview.SourceGeneratorFramework.PipelineCLI.Settings;
diff --git a/build/PipelineCLI/Helpers/DotNetCLIOptions.cs b/build/PipelineCLI/Helpers/DotNetCLIOptions.cs
deleted file mode 100644
index e967e66..0000000
--- a/build/PipelineCLI/Helpers/DotNetCLIOptions.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using ModularPipelines.Options;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Helpers;
-
-public sealed record DotNetCLIOptions : CommandLineToolOptions
-{
- public static DotNetCLIOptions Create(params string[] commandParts) =>
- new() { Tool = "dotnet", CommandParts = commandParts };
-}
diff --git a/build/PipelineCLI/Helpers/PathHelpers.cs b/build/PipelineCLI/Helpers/PathHelpers.cs
deleted file mode 100644
index aed0515..0000000
--- a/build/PipelineCLI/Helpers/PathHelpers.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Helpers;
-
-static class PathHelpers
-{
- public static string FindRepositoryRoot(string? startDirectory = null)
- {
- if (string.IsNullOrEmpty(startDirectory))
- startDirectory = PipelineProjectDirectory.Find();
-
- DirectoryInfo? directory = new(startDirectory);
- while (directory is not null)
- {
- if (File.Exists(Path.Combine(directory.FullName, "package.json")))
- return directory.FullName;
-
- directory = directory.Parent;
- }
-
- throw new InvalidOperationException("Could not locate the repository root (no package.json found).");
- }
-}
diff --git a/build/PipelineCLI/Helpers/TestHelpers.cs b/build/PipelineCLI/Helpers/TestHelpers.cs
deleted file mode 100644
index 5fa7e51..0000000
--- a/build/PipelineCLI/Helpers/TestHelpers.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Helpers;
-
-static class TestHelpers
-{
- public static string BuildTUnitTreeNodeFilter(
- string? assembly = null,
- string? @namespace = null,
- string? className = null,
- string? testNameQuery = null
- )
- {
- var filter = "/";
- filter += assembly switch
- {
- null => "*",
- _ => assembly,
- };
-
- filter += @namespace switch
- {
- null => "*",
- _ => @namespace,
- };
-
- filter += className switch
- {
- null => "*",
- _ => className,
- };
-
- filter += testNameQuery switch
- {
- null => "*",
- _ => testNameQuery,
- };
-
- return filter;
- }
-}
diff --git a/build/PipelineCLI/Modules/BuildModule.cs b/build/PipelineCLI/Modules/BuildModule.cs
deleted file mode 100644
index 6062972..0000000
--- a/build/PipelineCLI/Modules/BuildModule.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using ModularPipelines.Attributes;
-using ModularPipelines.Context;
-using ModularPipelines.DotNet.Extensions;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-[DependsOn]
-public class BuildModule(IOptions settings) : Module
-{
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- return await context
- .DotNet()
- .Build(
- new()
- {
- ProjectSolution = settings.Value.Solution,
- Configuration = settings.Value.Configuration,
- NoRestore = true,
- },
- cancellationToken: cancellationToken
- );
- }
-}
diff --git a/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs b/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs
deleted file mode 100644
index ae0259a..0000000
--- a/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using ModularPipelines.Attributes;
-using ModularPipelines.Configuration;
-using ModularPipelines.Context;
-using ModularPipelines.GitHub.Extensions;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Release")]
-[DependsOn]
-[DependsOn]
-[DependsOn]
-public class CreateGitHubReleaseModule(IOptions releaseSettings, IOptions gitSettings)
- : Module
-{
- protected override ModuleConfiguration Configure() =>
- ModuleConfiguration
- .Create()
- .WithSkipWhen(_ =>
- releaseSettings.Value.Mode is not (ReleaseMode.NuGet or ReleaseMode.GitHubRelease)
- || string.IsNullOrWhiteSpace(gitSettings.Value.GetGitHubToken())
- ? SkipDecision.Skip(
- "GitHub release creation is disabled. Set Release__Mode=NuGet (or GitHubRelease) and GITHUB_TOKEN to create a GitHub release."
- )
- : SkipDecision.DoNotSkip
- )
- .Build();
-
- protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
- {
- var versionResult = await context.GetModule();
- var version =
- versionResult.ValueOrDefault
- ?? throw new InvalidOperationException("The version was not produced by the version module.");
-
- var tag = $"v{version}";
-
- var repositoryIdString = context.GitHub().EnvironmentVariables.RepositoryId;
- if (!long.TryParse(repositoryIdString, out var repositoryId))
- {
- throw new InvalidOperationException(
- $"Failed to parse RepositoryId '{repositoryIdString}' as a valid long integer."
- );
- }
-
- // Create a new release on GitHub with the specified tag and generate release notes
- return await context
- .GitHub()
- .Client.Repository.Release.Create(
- repositoryId,
- new NewRelease(tag) { Name = tag, GenerateReleaseNotes = true }
- );
- }
-}
diff --git a/build/PipelineCLI/Modules/LintModule.cs b/build/PipelineCLI/Modules/LintModule.cs
deleted file mode 100644
index 91ff181..0000000
--- a/build/PipelineCLI/Modules/LintModule.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using ModularPipelines.Attributes;
-using ModularPipelines.Configuration;
-using ModularPipelines.Context;
-using ModularPipelines.DotNet.Extensions;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-public sealed class LintModule(IOptions settings) : Module
-{
- protected override ModuleConfiguration Configure() =>
- ModuleConfiguration
- .Create()
- .WithSkipWhen(_ =>
- settings.Value.RunLint
- ? SkipDecision.DoNotSkip
- : SkipDecision.Skip("Linting is disabled. Set Build__RunLint=true to enable it.")
- )
- .Build();
-
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- var repositoryRoot = PathHelpers.FindRepositoryRoot();
- var dotnet = context.DotNet();
- var restoreResult = await dotnet.Tool.Restore(
- new() { Interactive = false, ToolManifest = Path.Combine(repositoryRoot, ".config", "dotnet-tools.json") },
- new() { WorkingDirectory = repositoryRoot },
- cancellationToken
- );
- if (restoreResult.ExitCode != 0)
- return restoreResult;
-
- // Restore worked, now run the linter
- return await context.Shell.Command.ExecuteCommandLineTool(
- DotNetCLIOptions.Create("tool", "run", "csharpier", "check", repositoryRoot),
- new() { WorkingDirectory = repositoryRoot },
- cancellationToken: cancellationToken
- );
- }
-}
diff --git a/build/PipelineCLI/Modules/PackModule.cs b/build/PipelineCLI/Modules/PackModule.cs
deleted file mode 100644
index 166eec0..0000000
--- a/build/PipelineCLI/Modules/PackModule.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using ModularPipelines.Attributes;
-using ModularPipelines.Configuration;
-using ModularPipelines.Context;
-using ModularPipelines.DotNet.Extensions;
-using ModularPipelines.DotNet.Options;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-[DependsOn]
-[DependsOn]
-public sealed class PackModule(IOptions settings) : Module
-{
- protected override ModuleConfiguration Configure() =>
- ModuleConfiguration
- .Create()
- .WithSkipWhen(_ =>
- !settings.Value.RunPack
- ? SkipDecision.Skip("Packing is disabled. Set Build__RunPack=true to enable it.")
- : SkipDecision.DoNotSkip
- )
- .Build();
-
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- var versionResult = await context.GetModule();
- var nugetVersion =
- versionResult.ValueOrDefault
- ?? throw new InvalidOperationException("The version was not produced by the version module.");
-
- Directory.CreateDirectory(settings.Value.ArtifactsFolder);
-
- var version = nugetVersion.ToString();
- return await context
- .DotNet()
- .Pack(
- new DotNetPackOptions
- {
- ProjectSolution = settings.Value.Solution,
- Configuration = settings.Value.Configuration,
- Output = settings.Value.ArtifactsFolder,
- Properties = [("PackageVersion", version), ("Version", version)],
- },
- cancellationToken: cancellationToken
- );
- }
-}
diff --git a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs
deleted file mode 100644
index 5f1bbde..0000000
--- a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using ModularPipelines.Attributes;
-using ModularPipelines.Configuration;
-using ModularPipelines.Context;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-using NuGet.Versioning;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-[DependsOn]
-[DependsOn]
-public class PublishLocalNuGetModule(
- IOptions localNuGetFeedSettings,
- IOptions releaseSettings,
- IOptions buildSettings
-) : Module
-{
- protected override ModuleConfiguration Configure() =>
- ModuleConfiguration
- .Create()
- .WithSkipWhen(ctx =>
- !ctx.IsRunningLocally() || releaseSettings.Value.Mode != ReleaseMode.LocalNuGet
- ? SkipDecision.Skip(
- "Local NuGet Feed publishing is disabled. Run the pipeline locally with Release__Mode=LocalNuGet to enable it."
- )
- : SkipDecision.DoNotSkip
- )
- .Build();
-
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- var localFeedPath = localNuGetFeedSettings.Value.LocalFeedPath;
-
- var validationResults = new List();
- var validationContext = new ValidationContext(localNuGetFeedSettings.Value);
- if (
- !Validator.TryValidateObject(
- localNuGetFeedSettings.Value,
- validationContext,
- validationResults,
- validateAllProperties: true
- )
- )
- {
- foreach (var validationResult in validationResults)
- context.Logger.LogError("{Message}", validationResult.ErrorMessage);
-
- throw new InvalidOperationException(
- $"Invalid {nameof(PublishLocalNuGetSettings)} configuration for {nameof(PublishLocalNuGetSettings.LocalFeedPath)}. "
- + "Windows paths with backslashes may have been stripped by the shell; "
- + "use forward slashes, e.g. --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/."
- );
- }
-
- var fullLocalFeedPath = Path.GetFullPath(localFeedPath);
- context.Logger.LogInformation("Publishing local NuGet packages to {LocalFeedPath}.", fullLocalFeedPath);
-
- if (!Directory.Exists(fullLocalFeedPath))
- Directory.CreateDirectory(fullLocalFeedPath);
-
- var packages = Directory
- .GetFiles(buildSettings.Value.ArtifactsFolder, "*.nupkg")
- .Concat(Directory.GetFiles(buildSettings.Value.ArtifactsFolder, "*.snupkg"))
- .ToArray();
- if (packages.Length == 0)
- {
- throw new InvalidOperationException(
- $"No packages found in {buildSettings.Value.ArtifactsFolder}. The local feed was not populated."
- );
- }
-
- List nupkgPackages = [];
- foreach (var package in packages)
- {
- var fileName = Path.GetFileName(package);
- var destinationPath = Path.Combine(fullLocalFeedPath, fileName);
-
- if (Path.GetExtension(fileName) == ".nupkg")
- nupkgPackages.Add(await ParsePackageDetailsAsync(package, cancellationToken));
-
- if (!localNuGetFeedSettings.Value.OverwriteExistingPackages && File.Exists(destinationPath))
- {
- context.Logger.LogInformation("Package {Package} already exists in local feed. Skipping.", fileName);
- File.Delete(package);
-
- continue;
- }
-
- File.Move(package, destinationPath, true);
- context.Logger.LogInformation("Copied package {Package} to local feed.", fileName);
- }
-
- if (localNuGetFeedSettings.Value.ClearPackageCache)
- {
- context.Logger.LogInformation("Clearing local NuGet package cache...");
-
- var globalPackagesResult = await context.Shell.Command.ExecuteCommandLineTool(
- DotNetCLIOptions.Create("nuget", "locals", "global-packages", "--list"),
- cancellationToken: cancellationToken
- );
- if (globalPackagesResult.ExitCode != 0)
- return globalPackagesResult;
-
- var httpCacheResult = await context.Shell.Command.ExecuteCommandLineTool(
- DotNetCLIOptions.Create("nuget", "locals", "http-cache", "--list"),
- cancellationToken: cancellationToken
- );
- if (httpCacheResult.ExitCode != 0)
- return httpCacheResult;
-
- var globalPackagePaths = globalPackagesResult
- .StandardOutput.Replace("global-packages: ", "", StringComparison.Ordinal)
- .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
- .Where(Directory.Exists);
-
- var httpCachePaths = httpCacheResult
- .StandardOutput.Replace("http-cache: ", "", StringComparison.Ordinal)
- .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
- .Where(Directory.Exists);
-
- foreach (var artifact in nupkgPackages)
- {
-#pragma warning disable CA1308 // Normalize strings to uppercase
- var loweredPackageId = artifact.PackageId.ToLowerInvariant();
- var loweredVersion = artifact.Version.ToFullString().ToLowerInvariant();
-#pragma warning restore CA1308 // Normalize strings to uppercase
-
- foreach (var globalPath in globalPackagePaths)
- {
- var packagePath = Path.Combine(globalPath, loweredPackageId, artifact.Version.ToFullString());
- if (Directory.Exists(packagePath))
- {
- Directory.Delete(packagePath, true);
- context.Logger.LogInformation(
- "Deleted package {Package} version {Version} from global packages cache.",
- artifact.PackageId,
- artifact.Version
- );
- }
- }
- foreach (var httpCachePath in httpCachePaths)
- {
- string[] packagePaths =
- [
- Path.Combine(httpCachePath, "list_" + loweredPackageId + ".dat"),
- Path.Combine(httpCachePath, "list_" + loweredPackageId + "_index.dat"),
- Path.Combine(httpCachePath, "list_" + loweredPackageId + "_range_*.dat"),
- Path.Combine(httpCachePath, "nupkg_" + loweredPackageId + "." + loweredVersion + ".dat"),
- ];
-
- foreach (var path in packagePaths)
- {
- var directory = Path.GetDirectoryName(path);
- var pattern = Path.GetFileName(path);
- foreach (var file in Directory.EnumerateFiles(directory!, pattern, SearchOption.AllDirectories))
- {
- File.Delete(file);
- context.Logger.LogInformation(
- "Deleted package {Package} version {Version} from HTTP cache.",
- artifact.PackageId,
- artifact.Version
- );
- }
- }
- }
- }
- }
-
- if (localNuGetFeedSettings.Value.ShutdownDotnetBuilderServer)
- {
- context.Logger.LogInformation("Shutting down dotnet builder server...");
-
- return await context.Shell.Command.ExecuteCommandLineTool(
- DotNetCLIOptions.Create("build-server", "shutdown"),
- cancellationToken: cancellationToken
- );
- }
-
- return null;
- }
-
- static async Task ParsePackageDetailsAsync(string artifact, CancellationToken cancellationToken)
- {
- using var packageReader = new NuGet.Packaging.PackageArchiveReader(artifact);
- var packaging = await packageReader.GetNuspecReaderAsync(cancellationToken);
-
- return new(packaging.GetId(), packaging.GetVersion());
- }
-}
-
-record struct PackageDetails(string PackageId, NuGetVersion Version);
diff --git a/build/PipelineCLI/Modules/PublishNuGetModule.cs b/build/PipelineCLI/Modules/PublishNuGetModule.cs
deleted file mode 100644
index f2195ab..0000000
--- a/build/PipelineCLI/Modules/PublishNuGetModule.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using ModularPipelines.Attributes;
-using ModularPipelines.Configuration;
-using ModularPipelines.Context;
-using ModularPipelines.DotNet.Extensions;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Release")]
-[DependsOn]
-[DependsOn]
-[DependsOn]
-public class PublishNuGetModule(
- IOptions buildSettings,
- IOptions nugetSettings,
- IOptions releaseSettings
-) : Module
-{
- protected override ModuleConfiguration Configure() =>
- ModuleConfiguration
- .Create()
- .WithSkipWhen(_ =>
- releaseSettings.Value.Mode != ReleaseMode.NuGet
- || string.IsNullOrWhiteSpace(nugetSettings.Value.GetNuGetAPIKey())
- ? SkipDecision.Skip(
- "NuGet publishing is disabled. Set Release__Mode=NuGet and NuGet__ApiKey (or NUGET_APIKEY) to publish packages to nuget.org."
- )
- : SkipDecision.DoNotSkip
- )
- .Build();
-
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- var artifactsFolder = buildSettings.Value.ArtifactsFolder;
- if (!Directory.Exists(artifactsFolder))
- {
- throw new InvalidOperationException(
- $"The artifacts folder '{artifactsFolder}' does not exist. "
- + "Ensure the pack step ran (Release__Mode must not be None) before publishing."
- );
- }
-
- var packages = Directory.EnumerateFiles(artifactsFolder, "*.nupkg", SearchOption.TopDirectoryOnly).ToList();
-
- if (packages.Count == 0)
- {
- throw new InvalidOperationException($"No NuGet packages found in {buildSettings.Value.ArtifactsFolder}.");
- }
-
- var tasks = packages.Select(package =>
- context
- .DotNet()
- .Nuget.Push(
- new()
- {
- Path = package,
- Source = nugetSettings.Value.FeedUrl,
- ApiKey = nugetSettings.Value.GetNuGetAPIKey(),
- SkipDuplicate = true,
- },
- cancellationToken: cancellationToken
- )
- );
-
- return await Task.WhenAll(tasks);
- }
-}
diff --git a/build/PipelineCLI/Modules/RestoreModule.cs b/build/PipelineCLI/Modules/RestoreModule.cs
deleted file mode 100644
index eff5547..0000000
--- a/build/PipelineCLI/Modules/RestoreModule.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using ModularPipelines.Attributes;
-using ModularPipelines.Context;
-using ModularPipelines.DotNet.Extensions;
-using ModularPipelines.DotNet.Options;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-public class RestoreModule(IOptions settings) : Module
-{
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- return await context
- .DotNet()
- .Restore(
- new DotNetRestoreOptions { ProjectSolution = settings.Value.Solution },
- cancellationToken: cancellationToken
- );
- }
-}
diff --git a/build/PipelineCLI/Modules/RunTestsModule.cs b/build/PipelineCLI/Modules/RunTestsModule.cs
deleted file mode 100644
index e30dece..0000000
--- a/build/PipelineCLI/Modules/RunTestsModule.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using System.Diagnostics;
-using System.Text.RegularExpressions;
-using ModularPipelines.Attributes;
-using ModularPipelines.Configuration;
-using ModularPipelines.Context;
-using ModularPipelines.DotNet.Extensions;
-using ModularPipelines.DotNet.Options;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-[DependsOn]
-public class RunTestsModule(IOptions settings) : Module
-{
- protected override ModuleConfiguration Configure() =>
- ModuleConfiguration
- .Create()
- .WithSkipWhen(_ =>
- settings.Value.RunTests
- ? SkipDecision.DoNotSkip
- : SkipDecision.Skip("Tests are disabled. Set Build__RunTests=true to run them.")
- )
- .Build();
-
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- var testProjects = FilterTestProjects(
- Directory.EnumerateFiles("src/tests", "*Tests.csproj", SearchOption.AllDirectories).ToList(),
- settings.Value.TestProjects
- );
- if (testProjects.Count == 0)
- {
- context.Logger.LogWarning(
- "No test projects matched 'src/tests' (filter: {TestProjects}), despite tests being enabled. Skipping test execution.",
- settings.Value.TestProjects
- );
-
- return [];
- }
-
- var timings = new List<(string Project, TimeSpan Elapsed, int ExitCode)>();
-
- var tasks = testProjects.Select(async project =>
- {
- var stopwatch = Stopwatch.StartNew();
- var result = await context
- .DotNet()
- .Test(
- new DotNetTestOptions
- {
- Project = project,
- Configuration = settings.Value.Configuration,
- NoBuild = true,
- NoRestore = true,
- Arguments = ["--ignore-exit-code", "8", "--treenode-filter", settings.Value.TestFilter],
- },
- cancellationToken: cancellationToken
- );
- stopwatch.Stop();
-
- lock (timings)
- timings.Add((project, stopwatch.Elapsed, result.ExitCode));
-
- return result;
- });
-
- var results = await Task.WhenAll(tasks);
-
- context.Logger.LogInformation(
- "Test run timings:{NewLine}{Timings}",
- Environment.NewLine,
- string.Join(
- Environment.NewLine,
- timings
- .OrderByDescending(t => t.Elapsed)
- .Select(t => $" {Path.GetFileName(t.Project)}: {t.Elapsed.TotalSeconds:F1}s (exit {t.ExitCode})")
- )
- );
-
- return results;
- }
-
- static IReadOnlyList FilterTestProjects(IReadOnlyList projects, string filter)
- {
- if (string.IsNullOrWhiteSpace(filter) || filter.Trim() == "*")
- return projects;
-
- var patterns = filter
- .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
- .Select(ToRegexPattern)
- .ToArray();
-
- return projects
- .Where(project =>
- {
- var fileName = Path.GetFileName(project);
- return patterns.Any(pattern => Regex.IsMatch(fileName, pattern, RegexOptions.IgnoreCase));
- })
- .ToList();
- }
-
- static string ToRegexPattern(string entry)
- {
- if (entry.Contains('*', StringComparison.Ordinal))
- {
- var escaped = Regex.Escape(entry);
- return "^" + escaped.Replace("\\*", ".*", StringComparison.Ordinal) + "$";
- }
-
- return "^" + Regex.Escape(entry) + "$";
- }
-}
diff --git a/build/PipelineCLI/Modules/ValidatePackModule.cs b/build/PipelineCLI/Modules/ValidatePackModule.cs
deleted file mode 100644
index 236a1d6..0000000
--- a/build/PipelineCLI/Modules/ValidatePackModule.cs
+++ /dev/null
@@ -1,344 +0,0 @@
-using ModularPipelines.Attributes;
-using ModularPipelines.Configuration;
-using ModularPipelines.Context;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-using NuGet.Packaging;
-using NuGet.Versioning;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-[DependsOn]
-public sealed class ValidatePackModule(
- IOptions buildSettings,
- IOptions packValidationSettings
-) : Module
-{
- protected override ModuleConfiguration Configure() =>
- ModuleConfiguration
- .Create()
- .WithSkipWhen(_ =>
- !buildSettings.Value.ValidatePack
- ? SkipDecision.Skip("Pack validation is disabled. Set Build__ValidatePack=true to enable it.")
- : SkipDecision.DoNotSkip
- )
- .Build();
-
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- var artifactsFolder = Path.GetFullPath(buildSettings.Value.ArtifactsFolder);
- if (!Directory.Exists(artifactsFolder))
- {
- throw new InvalidOperationException(
- $"The artifacts folder '{artifactsFolder}' does not exist. Run the pack step first."
- );
- }
-
- var nupkgFiles = Directory.EnumerateFiles(artifactsFolder, "*.nupkg", SearchOption.TopDirectoryOnly).ToArray();
- var snupkgFiles = Directory
- .EnumerateFiles(artifactsFolder, "*.snupkg", SearchOption.TopDirectoryOnly)
- .ToArray();
-
- if (nupkgFiles.Length == 0)
- {
- throw new InvalidOperationException($"No .nupkg files found in {artifactsFolder}.");
- }
-
- var results = new List(nupkgFiles.Length + snupkgFiles.Length);
- var packagePairs = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- foreach (var package in nupkgFiles)
- {
- var result = await ValidateNupkgAsync(package, packValidationSettings.Value, cancellationToken);
- results.Add(result);
-
- var pair = GetOrAddPair(packagePairs, result.PackageKey);
- pair.Nupkg = result;
- }
-
- foreach (var package in snupkgFiles)
- {
- var result = await ValidateSnupkgAsync(package, packValidationSettings.Value, cancellationToken);
- results.Add(result);
-
- var pair = GetOrAddPair(packagePairs, result.PackageKey);
- pair.Snupkg = result;
- }
-
- if (packValidationSettings.Value.RequireSymbolPackage)
- {
- foreach (var pair in packagePairs.Values)
- {
- if (pair.Nupkg is not null && pair.Snupkg is null)
- {
- pair.Nupkg.AddError(
- $"Package '{pair.Nupkg.PackageId}' {pair.Nupkg.Version.ToNormalizedString()} has no matching .snupkg."
- );
- }
-
- if (pair.Snupkg is not null && pair.Nupkg is null)
- {
- pair.Snupkg.AddError(
- $"Symbol package '{pair.Snupkg.PackageId}' {pair.Snupkg.Version.ToNormalizedString()} has no matching .nupkg."
- );
- }
- }
- }
-
- var invalid = results.Where(result => result.Errors.Count > 0).ToList();
- foreach (var result in results)
- {
- if (result.Errors.Count == 0)
- {
- context.Logger.LogInformation(
- "Validated {FileName} ({Kind}): {PackageId} {Version}.",
- result.FileName,
- result.Kind,
- result.PackageId,
- result.Version.ToNormalizedString()
- );
- }
- else
- {
- foreach (var error in result.Errors)
- context.Logger.LogError("{FileName}: {Error}", result.FileName, error);
- }
- }
-
- var validCount = results.Count - invalid.Count;
- context.Summary.KeyValue("PackValidation", "Valid packages", $"{validCount}/{results.Count}");
- context.Summary.KeyValue("PackValidation", "Invalid packages", $"{invalid.Count}/{results.Count}");
-
- if (invalid.Count > 0)
- {
- var detail = string.Join(
- Environment.NewLine,
- invalid.Select(result =>
- $" {result.FileName}:{Environment.NewLine} "
- + string.Join(Environment.NewLine + " ", result.Errors)
- )
- );
-
- throw new InvalidOperationException(
- $"Pack validation failed for {invalid.Count} of {results.Count} package(s):{Environment.NewLine}{detail}"
- );
- }
-
- return results.ToArray();
- }
-
- static async Task ValidateNupkgAsync(
- string packagePath,
- PackValidationSettings settings,
- CancellationToken cancellationToken
- )
- {
- var errors = new List();
-
- try
- {
- using var reader = new PackageArchiveReader(packagePath);
- var nuspec = await reader.GetNuspecReaderAsync(cancellationToken);
- var id = nuspec.GetId();
- var version = nuspec.GetVersion();
-
- ValidateFileName(packagePath, id, version, ".nupkg", errors);
-
- var files = reader.GetFiles().ToArray();
- ValidateNoPdbFiles(files, errors);
-
- var required = GetContentRule(settings.RequiredContent, id);
- if (required is not null)
- {
- foreach (var entry in required)
- {
- if (!files.Contains(entry, StringComparer.OrdinalIgnoreCase))
- errors.Add($"Required content '{entry}' is missing from the package.");
- }
- }
-
- var forbidden = GetContentRule(settings.ForbiddenContent, id);
- if (forbidden is not null)
- {
- foreach (var entry in forbidden)
- {
- if (files.Contains(entry, StringComparer.OrdinalIgnoreCase))
- errors.Add($"Forbidden content '{entry}' must not be in the package.");
- }
- }
-
- var result = new PackValidationResult(
- Path.GetFileName(packagePath),
- "nupkg",
- CreatePackageKey(id, version),
- id,
- version
- );
- result.AddErrors(errors);
- return result;
- }
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- var result = new PackValidationResult(
- Path.GetFileName(packagePath),
- "nupkg",
- Path.GetFileName(packagePath) + "|unreadable",
- "",
- new NuGetVersion(0, 0, 0)
- );
- result.AddError($"Failed to read package: {ex.Message}");
- return result;
- }
- }
-
- static async Task ValidateSnupkgAsync(
- string packagePath,
- PackValidationSettings settings,
- CancellationToken cancellationToken
- )
- {
- var errors = new List();
-
- try
- {
- using var reader = new PackageArchiveReader(packagePath);
- var nuspec = await reader.GetNuspecReaderAsync(cancellationToken);
- var id = nuspec.GetId();
- var version = nuspec.GetVersion();
-
- ValidateFileName(packagePath, id, version, ".snupkg", errors);
-
- var files = reader.GetFiles().ToArray();
- var nonSymbolFiles = files.Where(file => !IsPdbFile(file) && !IsSymbolPackageMetadata(file)).ToArray();
- if (nonSymbolFiles.Length > 0)
- errors.Add($"Symbol package contains non-symbol file(s): {string.Join(", ", nonSymbolFiles)}.");
-
- if (settings.RequireSymbolFiles && !files.Any(IsPdbFile))
- errors.Add("Symbol package contains no .pdb files.");
-
- var result = new PackValidationResult(
- Path.GetFileName(packagePath),
- "snupkg",
- CreatePackageKey(id, version),
- id,
- version
- );
- result.AddErrors(errors);
- return result;
- }
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- var result = new PackValidationResult(
- Path.GetFileName(packagePath),
- "snupkg",
- Path.GetFileName(packagePath) + "|unreadable",
- "",
- new NuGetVersion(0, 0, 0)
- );
- result.AddError($"Failed to read package: {ex.Message}");
- return result;
- }
- }
-
- static void ValidateFileName(
- string packagePath,
- string id,
- NuGetVersion version,
- string extension,
- List errors
- )
- {
- var expected = $"{id}.{version.ToNormalizedString()}{extension}";
- if (!string.Equals(Path.GetFileName(packagePath), expected, StringComparison.OrdinalIgnoreCase))
- errors.Add(
- $"File name '{Path.GetFileName(packagePath)}' does not match the nuspec id/version '{expected}'."
- );
- }
-
- static void ValidateNoPdbFiles(IEnumerable files, List errors)
- {
- var pdbFiles = files.Where(IsPdbFile).ToArray();
- if (pdbFiles.Length > 0)
- errors.Add(
- $"Package contains PDB file(s): {string.Join(", ", pdbFiles)}. "
- + "PDBs must only be delivered through the .snupkg."
- );
- }
-
- static bool IsPdbFile(string path) =>
- string.Equals(Path.GetExtension(path), ".pdb", StringComparison.OrdinalIgnoreCase);
-
- static bool IsSymbolPackageMetadata(string path) =>
- string.Equals(path, "[Content_Types].xml", StringComparison.OrdinalIgnoreCase)
- || path.StartsWith("_rels/", StringComparison.OrdinalIgnoreCase)
- || path.StartsWith("package/services/metadata/", StringComparison.OrdinalIgnoreCase)
- || path.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase);
-
- static string[]? GetContentRule(Dictionary rules, string packageId)
- {
- if (rules.TryGetValue(packageId, out var exact))
- return exact;
-
- foreach (var rule in rules)
- {
- if (string.Equals(rule.Key, packageId, StringComparison.OrdinalIgnoreCase))
- return rule.Value;
- }
-
- return null;
- }
-
- static string CreatePackageKey(string id, NuGetVersion version) => $"{id}|{version.ToNormalizedString()}";
-
- static PackagePair GetOrAddPair(Dictionary pairs, string key)
- {
- if (!pairs.TryGetValue(key, out var pair))
- {
- pair = new PackagePair();
- pairs.Add(key, pair);
- }
-
- return pair;
- }
-}
-
-public sealed class PackValidationResult
-{
- readonly List _errors = [];
-
- public PackValidationResult(string fileName, string kind, string packageKey, string packageId, NuGetVersion version)
- {
- FileName = fileName;
- Kind = kind;
- PackageKey = packageKey;
- PackageId = packageId;
- Version = version;
- }
-
- public string FileName { get; }
-
- public string Kind { get; }
-
- public string PackageKey { get; }
-
- public string PackageId { get; }
-
- public NuGetVersion Version { get; }
-
- public IReadOnlyList Errors => _errors;
-
- internal void AddError(string error) => _errors.Add(error);
-
- internal void AddErrors(IEnumerable errors) => _errors.AddRange(errors);
-}
-
-sealed class PackagePair
-{
- public PackValidationResult? Nupkg { get; set; }
-
- public PackValidationResult? Snupkg { get; set; }
-}
diff --git a/build/PipelineCLI/Modules/VersionModule.cs b/build/PipelineCLI/Modules/VersionModule.cs
deleted file mode 100644
index 83d3631..0000000
--- a/build/PipelineCLI/Modules/VersionModule.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Text.Json;
-using ModularPipelines.Attributes;
-using ModularPipelines.Context;
-using ModularPipelines.Modules;
-using NuGet.Versioning;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules;
-
-[ModuleCategory("Build")]
-public class VersionModule : Module
-{
- protected override async Task ExecuteAsync(
- IModuleContext context,
- CancellationToken cancellationToken
- )
- {
- var packageJsonPath = Path.Combine(Environment.CurrentDirectory, "package.json");
-
- if (!File.Exists(packageJsonPath))
- throw new FileNotFoundException($"Could not find package.json at {packageJsonPath}");
-
- var packageJson = await File.ReadAllTextAsync(packageJsonPath, cancellationToken);
-
- using var document = JsonDocument.Parse(packageJson);
- var version = document.RootElement.GetProperty("version").GetString();
-
- if (string.IsNullOrWhiteSpace(version))
- throw new InvalidOperationException("The version field in package.json is missing or empty.");
-
- if (!NuGetVersion.TryParse(version, out var nugetVersion))
- throw new InvalidOperationException($"The version '{version}' in package.json is not a valid SemVer.");
-
- context.Summary.KeyValue("Version", "Package version", version);
- return nugetVersion;
- }
-}
diff --git a/build/PipelineCLI/PipelineCLI.csproj b/build/PipelineCLI/PipelineCLI.csproj
deleted file mode 100644
index 10d8460..0000000
--- a/build/PipelineCLI/PipelineCLI.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
-
diff --git a/build/PipelineCLI/PipelineProjectDirectory.cs b/build/PipelineCLI/PipelineProjectDirectory.cs
deleted file mode 100644
index b89c24e..0000000
--- a/build/PipelineCLI/PipelineProjectDirectory.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System.Runtime.CompilerServices;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI;
-
-static class PipelineProjectDirectory
-{
- const string DirectoryVariable = "MODULAR_PIPELINES_DIRECTORY";
-
- public static string Find([CallerFilePath] string sourceFilePath = "")
- {
- var configuredDirectory = Environment.GetEnvironmentVariable(DirectoryVariable);
- if (!string.IsNullOrWhiteSpace(configuredDirectory))
- {
- return ValidateConfiguredDirectory(configuredDirectory);
- }
-
- var sourceDirectory = Path.GetDirectoryName(sourceFilePath);
- return IsPipelineDirectory(sourceDirectory) ? sourceDirectory! : FindFromBuildOutput();
- }
-
- static string ValidateConfiguredDirectory(string configuredDirectory)
- {
- var fullPath = Path.GetFullPath(configuredDirectory);
- return IsPipelineDirectory(fullPath)
- ? fullPath
- : throw new InvalidOperationException(
- $"{DirectoryVariable} must point to a directory containing appsettings.json and a project file."
- );
- }
-
- static string FindFromBuildOutput()
- {
- for (
- var directory = new DirectoryInfo(AppContext.BaseDirectory);
- directory is not null;
- directory = directory.Parent
- )
- {
- if (IsPipelineDirectory(directory.FullName))
- {
- return directory.FullName;
- }
- }
-
- throw new InvalidOperationException(
- $"Could not locate the pipeline project directory. Set {DirectoryVariable} to its path."
- );
- }
-
- static bool IsPipelineDirectory(string? directory) =>
- directory is not null
- && Directory.Exists(directory)
- && File.Exists(Path.Combine(directory, "appsettings.json"))
- && Directory.EnumerateFiles(directory, "*.csproj").Any();
-}
diff --git a/build/PipelineCLI/Program.cs b/build/PipelineCLI/Program.cs
deleted file mode 100644
index 1997d14..0000000
--- a/build/PipelineCLI/Program.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-var pipelineDirectory = PipelineProjectDirectory.Find();
-var repositoryRoot = PathHelpers.FindRepositoryRoot(pipelineDirectory);
-
-var builder = Pipeline.CreateBuilder(args);
-
-builder
- .Configuration.AddJsonFile(Path.Combine(pipelineDirectory, "appsettings.json"), optional: false)
- .AddEnvironmentVariables()
- .AddCommandLine(args);
-
-builder.Services.Configure(builder.Configuration.GetSection(BuildSettings.SectionName));
-builder.Services.Configure(builder.Configuration.GetSection(NuGetSettings.SectionName));
-builder.Services.Configure(
- builder.Configuration.GetSection(PackValidationSettings.SectionName)
-);
-builder.Services.Configure(
- builder.Configuration.GetSection(PublishLocalNuGetSettings.SectionName)
-);
-builder.Services.Configure(builder.Configuration.GetSection(GitHubSettings.SectionName));
-builder.Services.Configure(builder.Configuration.GetSection(ReleaseSettings.SectionName));
-
-builder.Services.AddSingleton(serviceProvider =>
-{
- var settings = serviceProvider.GetRequiredService>();
- var accessToken = settings.Value.GetGitHubToken();
-
- return new GitHubClient(new(settings.Value.ProductHeader), new InMemoryCredentialStore(new(accessToken)));
-});
-
-Environment.CurrentDirectory = repositoryRoot;
-
-builder
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule();
-
-await using var pipeline = await builder.BuildAsync();
-
-await pipeline.RunAsync();
diff --git a/build/PipelineCLI/Properties/launchSettings.json b/build/PipelineCLI/Properties/launchSettings.json
deleted file mode 100644
index d5bbebe..0000000
--- a/build/PipelineCLI/Properties/launchSettings.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "profiles": {
- "Run": {
- "commandName": "Project"
- },
- "Local-NuGet": {
- "commandName": "Project",
- "commandLineArgs": "--Release:Mode=LocalNuGet\r\n--PublishLocalNuGet:LocalFeedPath=p:\\_sync-projects\\.local-nuget\\"
- }
- }
-}
diff --git a/build/PipelineCLI/Settings/BuildSettings.cs b/build/PipelineCLI/Settings/BuildSettings.cs
deleted file mode 100644
index cfb27da..0000000
--- a/build/PipelineCLI/Settings/BuildSettings.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings;
-
-public sealed class BuildSettings
-{
- public const string SectionName = "Build";
-
- public LogLevel LogLevel { get; init; } = LogLevel.Warning;
-
- [Required(AllowEmptyStrings = false)]
- public string Solution { get; init; } = "src/SourceGeneratorFramework.slnx";
-
- [Required(AllowEmptyStrings = false)]
- public string Configuration { get; init; } = "Release";
-
- [Required(AllowEmptyStrings = false)]
- public string ArtifactsFolder { get; init; } = "artifacts";
-
- public bool RunTests { get; init; } = true;
-
- [Required(AllowEmptyStrings = false)]
- public string TestFilter { get; init; } = "/*/*/*/*/";
-
- ///
- /// Comma-separated list of test project file names (or glob patterns) to run.
- /// Empty or "*" runs every test project under src/tests.
- ///
- public string TestProjects { get; init; } = "*";
-
- public bool RunLint { get; init; } = true;
-
- public bool RunPack { get; init; } = true;
-
- public bool ValidatePack { get; init; } = true;
-}
diff --git a/build/PipelineCLI/Settings/GitHubSettings.cs b/build/PipelineCLI/Settings/GitHubSettings.cs
deleted file mode 100644
index 0c1baa1..0000000
--- a/build/PipelineCLI/Settings/GitHubSettings.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using ModularPipelines.Attributes;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings;
-
-public sealed record GitHubSettings
-{
- public const string SectionName = "GitHub";
-
- [SecretValue]
- public string? AccessToken { get; init; }
-
- [SecretValue]
- [ConfigurationKeyName("GITHUB_TOKEN")]
- public string? EnvAccessToken { get; init; }
-
- public string ProductHeader { get; init; } = "Purview.SourceGeneratorFramework.Pipeline";
-
- public string? GetGitHubToken()
- {
- if (!string.IsNullOrWhiteSpace(AccessToken))
- return AccessToken;
-
- if (!string.IsNullOrWhiteSpace(EnvAccessToken))
- return EnvAccessToken;
-
- // GitHub Actions provisions the automatic GITHUB_TOKEN as a plain environment variable.
- // The config binder keys it under the "GitHub" section (GitHub:GITHUB_TOKEN), which the
- // standard GITHUB_TOKEN env var does not map to, so read it directly as a fallback.
- var processToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
- return string.IsNullOrWhiteSpace(processToken) ? null : processToken;
- }
-}
diff --git a/build/PipelineCLI/Settings/NuGetSettings.cs b/build/PipelineCLI/Settings/NuGetSettings.cs
deleted file mode 100644
index 1ccac86..0000000
--- a/build/PipelineCLI/Settings/NuGetSettings.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using ModularPipelines.Attributes;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings;
-
-public sealed record NuGetSettings
-{
- public const string SectionName = "NuGet";
-
- [SecretValue]
- public string? APIKey { get; set; }
-
- [SecretValue]
- [ConfigurationKeyName("NUGET_APIKEY")]
- public string? EnvAPIKey { get; set; }
-
- public string FeedUrl { get; init; } = "https://api.nuget.org/v3/index.json";
-
- public string? GetNuGetAPIKey() =>
- !string.IsNullOrWhiteSpace(APIKey) ? APIKey
- : !string.IsNullOrWhiteSpace(EnvAPIKey) ? EnvAPIKey
- : null;
-}
diff --git a/build/PipelineCLI/Settings/PackValidationSettings.cs b/build/PipelineCLI/Settings/PackValidationSettings.cs
deleted file mode 100644
index 7f1b493..0000000
--- a/build/PipelineCLI/Settings/PackValidationSettings.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings;
-
-public sealed record PackValidationSettings
-{
- public const string SectionName = "PackValidation";
-
- ///
- /// Every .nupkg must have a matching .snupkg (same id/version) and vice versa.
- ///
- public bool RequireSymbolPackage { get; init; } = true;
-
- ///
- /// Every .snupkg must contain at least one .pdb file.
- ///
- public bool RequireSymbolFiles { get; init; } = true;
-
- ///
- /// Package id (case-insensitive) to entry paths that MUST be present in the .nupkg.
- /// Entry paths use forward slashes, e.g. "lib/netstandard2.0/Foo.dll".
- ///
- public Dictionary RequiredContent { get; init; } = [];
-
- ///
- /// Package id (case-insensitive) to entry paths that MUST NOT be present in the .nupkg.
- /// Entry paths use forward slashes, e.g. "lib/netstandard2.0/Foo.dll".
- ///
- public Dictionary ForbiddenContent { get; init; } = [];
-}
diff --git a/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs b/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs
deleted file mode 100644
index 4d0256f..0000000
--- a/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings;
-
-public sealed record PublishLocalNuGetSettings : IValidatableObject
-{
- public const string SectionName = "PublishLocalNuGet";
-
- [Required(AllowEmptyStrings = false)]
- public string LocalFeedPath { get; init; } = string.Empty;
-
- public bool OverwriteExistingPackages { get; init; } = true;
-
- public bool ShutdownDotnetBuilderServer { get; init; } = true;
-
- public bool ClearPackageCache { get; init; } = true;
-
- public IEnumerable Validate(ValidationContext validationContext)
- {
- if (string.IsNullOrWhiteSpace(LocalFeedPath))
- {
- yield return new ValidationResult("LocalFeedPath is required.", [nameof(LocalFeedPath)]);
- yield break;
- }
-
- // Path.IsPathRooted("p:foo") returns true, but a drive-relative path like "p:foo" is NOT an
- // absolute path: Path.GetFullPath resolves it against the current directory and can silently
- // copy packages to an unintended location. This is the classic signature of a Windows path whose
- // backslashes were stripped by a sh-style shell, e.g. 'p:\_sync-projects\.local-nuget\'.
- if (LocalFeedPath.Length >= 2 && LocalFeedPath[1] == ':')
- {
- var hasSeparatorAfterDrive =
- LocalFeedPath.Length >= 3
- && (
- LocalFeedPath[2] == Path.DirectorySeparatorChar
- || LocalFeedPath[2] == Path.AltDirectorySeparatorChar
- );
- if (!hasSeparatorAfterDrive)
- {
- yield return new ValidationResult(
- $"LocalFeedPath '{LocalFeedPath}' is drive-relative, not an absolute path. "
- + "This is usually caused by the shell stripping backslashes from a Windows path such as "
- + $"'p:\\_sync-projects\\.local-nuget\\'. Use forward slashes instead, e.g. "
- + "'p:/_sync-projects/.local-nuget/'.",
- [nameof(LocalFeedPath)]
- );
- yield break;
- }
- }
-
- if (!Path.IsPathRooted(LocalFeedPath))
- {
- yield return new ValidationResult(
- $"LocalFeedPath must be an absolute path. Received: '{LocalFeedPath}'.",
- [nameof(LocalFeedPath)]
- );
- yield break;
- }
-
- var root = Path.GetPathRoot(LocalFeedPath);
- if (string.IsNullOrEmpty(root))
- {
- yield return new ValidationResult(
- $"LocalFeedPath could not be parsed. Received: '{LocalFeedPath}'.",
- [nameof(LocalFeedPath)]
- );
- yield break;
- }
-
- var lastChar = root[^1];
- if (lastChar == Path.DirectorySeparatorChar || lastChar == Path.AltDirectorySeparatorChar)
- yield break;
-
- if (root.StartsWith(@"\\", StringComparison.Ordinal) || root.StartsWith("//", StringComparison.Ordinal))
- yield break;
-
- yield return new ValidationResult(
- $"LocalFeedPath must be an absolute path (e.g. 'C:\\folder' or '\\\\server\\share'). Received: '{LocalFeedPath}'.",
- [nameof(LocalFeedPath)]
- );
- }
-}
diff --git a/build/PipelineCLI/Settings/ReleaseSettings.cs b/build/PipelineCLI/Settings/ReleaseSettings.cs
deleted file mode 100644
index 7cf2acc..0000000
--- a/build/PipelineCLI/Settings/ReleaseSettings.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings;
-
-public enum ReleaseMode
-{
- None,
-
- NuGet,
-
- GitHubRelease,
-
- LocalNuGet,
-}
-
-public sealed record ReleaseSettings
-{
- public const string SectionName = "Release";
-
- public ReleaseMode Mode { get; set; } = ReleaseMode.None;
-}
diff --git a/build/PipelineCLI/appsettings.json b/purview-build.json
similarity index 71%
rename from build/PipelineCLI/appsettings.json
rename to purview-build.json
index 50927c2..286a5c3 100644
--- a/build/PipelineCLI/appsettings.json
+++ b/purview-build.json
@@ -1,14 +1,9 @@
{
"Build": {
"Solution": "src/SourceGeneratorFramework.slnx",
- "Configuration": "Release",
- "ArtifactsFolder": "artifacts",
- "RunTests": true,
- "TestFilter": "/*/*/*/*/",
- "TestProjects": "*",
- "RunLint": true,
- "RunPack": true,
- "ValidatePack": true
+ "TestRoot": "src/tests",
+ "TestPatterns": "*Tests.csproj",
+ "TestFilter": "/*/*/*/*/"
},
"PackValidation": {
"RequireSymbolPackage": true,
@@ -37,20 +32,7 @@
},
"ForbiddenContent": {}
},
- "NuGet": {
- "FeedUrl": "https://api.nuget.org/v3/index.json"
- },
- "PublishLocalNuGet": {
- "LocalFeedPath": "",
- "OverwriteExistingPackages": true,
- "ShutdownDotnetBuilderServer": true,
- "ClearPackageCache": true
- },
- "GitHub": {
- "AccessToken": null,
- "ProductHeader": "Purview.SourceGeneratorFramework.Pipeline"
- },
"Release": {
"Mode": "None"
}
-}
+}
\ No newline at end of file
diff --git a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md
index 9fab643..5595ecf 100644
--- a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md
@@ -8,6 +8,8 @@ PSGFR12 | Purview.SourceGeneratorFramework | Warning | Use IIncrementalGenerator
PSGFR14 | Purview.SourceGeneratorFramework | Warning | Avoid RegisterImplementationSourceOutput
PSGFR15 | Purview.SourceGeneratorFramework | Warning | Pipeline model collection lacks sequence equality
PSGFR16 | Purview.SourceGeneratorFramework | Info | Prefer the nullable-context overload
+PSGFR17 | Purview.SourceGeneratorFramework | Warning | Consume CodeWriter scopes with a using statement
+PSGFR18 | Purview.SourceGeneratorFramework | Info | Prefer a structured CodeWriter declaration API
ADM0001 | Target | Error | Target attribute type cannot be resolved
ADM0002 | Property | Error | Property type is not supported for attribute extraction
ADM0003 | Source | Error | Specified constructor index/name does not exist on the target attribute
diff --git a/src/src/SourceGeneratorFramework.Analyzers/DiscardedCodeWriterScopeAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/DiscardedCodeWriterScopeAnalyzer.cs
new file mode 100644
index 0000000..46972e8
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.Analyzers/DiscardedCodeWriterScopeAnalyzer.cs
@@ -0,0 +1,66 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.SourceGeneratorFramework.Analyzers;
+
+///
+/// Flags CodeWriter scope-returning methods whose returned scope is discarded, which skips the
+/// block's closing token and can unbalance indentation.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class DiscardedCodeWriterScopeAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "PSGFR17";
+
+ public static readonly DiagnosticDescriptor Rule = new(
+ DiagnosticId,
+ "Consume CodeWriter scopes with a using statement",
+ "The scope returned by {0} is discarded; assign it to a using statement so the closing token is written",
+ "Purview.SourceGeneratorFramework",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Scope-returning CodeWriter methods must be consumed by a using statement so the generated block is closed correctly."
+ );
+
+ public override ImmutableArray SupportedDiagnostics => [Rule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ if (context is null)
+ throw new ArgumentNullException(nameof(context));
+
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeExpressionStatement, SyntaxKind.ExpressionStatement);
+ }
+
+ static void AnalyzeExpressionStatement(SyntaxNodeAnalysisContext context)
+ {
+ if (context.Node is not ExpressionStatementSyntax { Expression: InvocationExpressionSyntax invocation })
+ return;
+
+ if (
+ context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol
+ is not IMethodSymbol method
+ )
+ return;
+
+ if (method.ContainingType?.ToDisplayString() != "Purview.SourceGeneratorFramework.CodeWriter")
+ return;
+
+ var returnType = method.ReturnType.ToDisplayString();
+ if (
+ returnType
+ is not (
+ "Purview.SourceGeneratorFramework.CodeWriter.BlockScope"
+ or "Purview.SourceGeneratorFramework.CodeWriter.IndentScope"
+ )
+ )
+ return;
+
+ context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), method.Name));
+ }
+}
diff --git a/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterApiAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterApiAnalyzer.cs
new file mode 100644
index 0000000..013f302
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterApiAnalyzer.cs
@@ -0,0 +1,115 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.SourceGeneratorFramework.Analyzers;
+
+///
+/// Flags raw CodeWriter text emission that starts with a C# declaration keyword, suggesting a
+/// structured declaration API such as WriteClass or WriteProperty instead.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class PreferStructuredCodeWriterApiAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "PSGFR18";
+
+ public static readonly DiagnosticDescriptor Rule = new(
+ DiagnosticId,
+ "Prefer a structured CodeWriter declaration API",
+ "'{0}' with a declaration should use a structured API such as WriteClass, WriteMethod, WriteProperty, or WriteField",
+ "Purview.SourceGeneratorFramework",
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true,
+ description: "Emitting declaration syntax through raw text bypasses the structured, deterministic declaration APIs on CodeWriter."
+ );
+
+ static readonly string[] DeclarationStarts =
+ [
+ "public ",
+ "internal ",
+ "private ",
+ "protected ",
+ "file ",
+ "static ",
+ "sealed ",
+ "abstract ",
+ "partial ",
+ "readonly ",
+ "ref ",
+ "required ",
+ "const ",
+ "class ",
+ "struct ",
+ "interface ",
+ "enum ",
+ "record ",
+ "delegate ",
+ "namespace ",
+ "global using ",
+ "using ",
+ ];
+
+ public override ImmutableArray SupportedDiagnostics => [Rule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ if (context is null)
+ throw new ArgumentNullException(nameof(context));
+
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
+ {
+ if (
+ context.Node
+ is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax member } invocation
+ )
+ return;
+
+ var name = member.Name.Identifier.Text;
+ if (name is not ("Write" or "WriteLine" or "Append" or "AppendLine" or "MultiLine"))
+ return;
+
+ if (invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is not LiteralExpressionSyntax literal)
+ return;
+
+ if (!literal.IsKind(SyntaxKind.StringLiteralExpression))
+ return;
+
+ if (
+ context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol
+ is not IMethodSymbol method
+ )
+ return;
+
+ if (method.ContainingType?.ToDisplayString() != "Purview.SourceGeneratorFramework.CodeWriter")
+ return;
+
+ var value = literal.Token.ValueText.TrimStart();
+ if (StartsWithDeclaration(value))
+ context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), name));
+ }
+
+ static bool StartsWithDeclaration(string value)
+ {
+ foreach (var prefix in DeclarationStarts)
+ {
+ if (!value.StartsWith(prefix, StringComparison.Ordinal))
+ continue;
+
+ // "using (var x = ...)" is a using statement inside a body, not a directive; don't flag it.
+ if (prefix == "using " && value.StartsWith("using (", StringComparison.Ordinal))
+ return false;
+
+ // "namespace global::" is a valid namespace declaration, but "global::" is not a declaration keyword.
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator.CodeFixers/LoggingRefactoringProvider.cs b/src/src/SourceGeneratorFramework.ExampleGenerator.CodeFixers/LoggingRefactoringProvider.cs
index bf1b9d0..0392b96 100644
--- a/src/src/SourceGeneratorFramework.ExampleGenerator.CodeFixers/LoggingRefactoringProvider.cs
+++ b/src/src/SourceGeneratorFramework.ExampleGenerator.CodeFixers/LoggingRefactoringProvider.cs
@@ -13,7 +13,9 @@ namespace Purview.SourceGeneratorFramework.ExampleGenerator.CodeFixers;
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(LoggingRefactoringProvider))]
public sealed class LoggingRefactoringProvider : CodeRefactoringProvider
{
- /// The equivalence key of the registered code action.
+ ///
+ /// The equivalence key of the registered code action.
+ ///
public const string EquivalenceKey = "AddDebug";
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/LoggingAttributes.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/LoggingAttributes.cs
index 87eb4a6..7886c67 100644
--- a/src/src/SourceGeneratorFramework.ExampleGenerator/LoggingAttributes.cs
+++ b/src/src/SourceGeneratorFramework.ExampleGenerator/LoggingAttributes.cs
@@ -5,22 +5,34 @@ namespace Purview.SourceGeneratorFramework.Examples;
///
public enum LogLevel
{
- /// Trace-level detail.
+ ///
+ /// Trace-level detail.
+ ///
Trace = 0,
- /// Debug-level detail.
+ ///
+ /// Debug-level detail.
+ ///
Debug = 1,
- /// Informational messages.
+ ///
+ /// Informational messages.
+ ///
Information = 2,
- /// Warnings.
+ ///
+ /// Warnings.
+ ///
Warning = 3,
- /// Errors.
+ ///
+ /// Errors.
+ ///
Error = 4,
- /// Critical failures.
+ ///
+ /// Critical failures.
+ ///
Critical = 5,
}
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/CodeQueryAssertions.cs b/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/CodeQueryAssertions.cs
index 146a602..f718b26 100644
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/CodeQueryAssertions.cs
+++ b/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/CodeQueryAssertions.cs
@@ -15,7 +15,9 @@ public static partial class CodeQueryAssertions
// Generated code (source generators)
// ---------------------------------------------------------------------------------------------
- /// Asserts that the generated code contains a method with the given name, returning it.
+ ///
+ /// Asserts that the generated code contains a method with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasGeneratedMethod(
@@ -23,7 +25,9 @@ public static AssertionResult HasGeneratedMethod(
string methodName
) => GetMethod(result?.Generated(), methodName, null, "generated code");
- /// Asserts that the generated code contains a method with the given name and parameter types, returning it.
+ ///
+ /// Asserts that the generated code contains a method with the given name and parameter types, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasGeneratedMethod(
@@ -32,7 +36,9 @@ public static AssertionResult HasGeneratedMethod(
TypeReference[] parameters
) => GetMethod(result?.Generated(), methodName, parameters, "generated code");
- /// Asserts that the generated code contains a method with the given name and return type, returning it.
+ ///
+ /// Asserts that the generated code contains a method with the given name and return type, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasGeneratedMethodReturnType(
@@ -58,7 +64,9 @@ TypeReference returnType
);
}
- /// Asserts that the generated code contains a class with the given name, returning it.
+ ///
+ /// Asserts that the generated code contains a class with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasGeneratedClass(
@@ -81,7 +89,9 @@ string className
AssertionResult.Failed($"generated code did not contain a class named '{className}'");
}
- /// Asserts that the generated code contains a property with the given name, returning it.
+ ///
+ /// Asserts that the generated code contains a property with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasGeneratedProperty(
@@ -105,7 +115,9 @@ string propertyName
AssertionResult.Failed($"generated code did not contain a property named '{propertyName}'");
}
- /// Asserts that the generated code contains a field with the given name, returning it.
+ ///
+ /// Asserts that the generated code contains a field with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasGeneratedField(
@@ -128,7 +140,9 @@ string fieldName
AssertionResult.Failed($"generated code did not contain a field named '{fieldName}'");
}
- /// Asserts that the generated code contains a syntax tree with the given name, returning it.
+ ///
+ /// Asserts that the generated code contains a syntax tree with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasGeneratedSyntaxTree(this DriverRunResult result, string treeName)
@@ -151,7 +165,9 @@ public static AssertionResult HasGeneratedSyntaxTree(this DriverRunR
// Fixed code (code fixes and refactorings)
// ---------------------------------------------------------------------------------------------
- /// Asserts that the fixed code contains a method with the given name, returning it.
+ ///
+ /// Asserts that the fixed code contains a method with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasFixedMethod(
@@ -159,7 +175,9 @@ public static AssertionResult HasFixedMethod(
string methodName
) => GetMethod(result?.FixedCode(), methodName, null, "fixed code");
- /// Asserts that the fixed code contains a method with the given name and parameter types, returning it.
+ ///
+ /// Asserts that the fixed code contains a method with the given name and parameter types, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasFixedMethod(
@@ -168,7 +186,9 @@ public static AssertionResult HasFixedMethod(
TypeReference[] parameters
) => GetMethod(result?.FixedCode(), methodName, parameters, "fixed code");
- /// Asserts that the fixed code contains a method with the given name, returning it.
+ ///
+ /// Asserts that the fixed code contains a method with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasFixedMethod(
@@ -176,7 +196,9 @@ public static AssertionResult HasFixedMethod(
string methodName
) => GetMethod(result?.FixedCode(), methodName, null, "fixed code");
- /// Asserts that the fixed code contains a method with the given name, returning it.
+ ///
+ /// Asserts that the fixed code contains a method with the given name, returning it.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasFixedMethod(
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/DiagnosticAssertions.cs b/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/DiagnosticAssertions.cs
index d2ca40b..bd8ab8f 100644
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/DiagnosticAssertions.cs
+++ b/src/src/SourceGeneratorFramework.Testing.TUnit/Assertions/DiagnosticAssertions.cs
@@ -266,7 +266,9 @@ public static AssertionResult HasNoDiagnostics(this DriverRunResult result)
);
}
- /// Asserts that an analyzer result contains the expected diagnostic.
+ ///
+ /// Asserts that an analyzer result contains the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasDiagnostic(
@@ -274,7 +276,9 @@ public static AssertionResult HasDiagnostic(
DiagnosticDescriptor expected
) => HasDiagnostic(diagnostic?.Diagnostics, expected);
- /// Asserts that an analyzer result contains the expected total number of diagnostics.
+ ///
+ /// Asserts that an analyzer result contains the expected total number of diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult> HasDiagnostics(
@@ -282,7 +286,9 @@ public static AssertionResult> HasDiagnostics(
int count
) => HasDiagnostics(diagnostic?.Diagnostics, count);
- /// Asserts that an analyzer result contains the expected number of diagnostics.
+ ///
+ /// Asserts that an analyzer result contains the expected number of diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult> HasDiagnostics(
@@ -291,13 +297,17 @@ public static AssertionResult> HasDiagnostics(
int count
) => HasDiagnostics(diagnostic?.Diagnostics, expected, count);
- /// Asserts that an analyzer result contains the expected diagnostic.
+ ///
+ /// Asserts that an analyzer result contains the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasDiagnostic(this AnalyzerTestResult diagnostic, string expected) =>
HasDiagnostic(diagnostic?.Diagnostics, expected);
- /// Asserts that an analyzer result contains the expected number of diagnostics.
+ ///
+ /// Asserts that an analyzer result contains the expected number of diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult> HasDiagnostics(
@@ -306,7 +316,9 @@ public static AssertionResult> HasDiagnostics(
int count
) => HasDiagnostics(diagnostic?.Diagnostics, expected, count);
- /// Asserts that an analyzer result does not contain the expected diagnostic.
+ ///
+ /// Asserts that an analyzer result does not contain the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult DoesNotHaveDiagnostic(
@@ -314,13 +326,17 @@ public static AssertionResult DoesNotHaveDiagnostic(
DiagnosticDescriptor expected
) => DoesNotHaveDiagnostic(result?.Diagnostics, expected);
- /// Asserts that an analyzer result does not contain the expected diagnostic.
+ ///
+ /// Asserts that an analyzer result does not contain the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult DoesNotHaveDiagnostic(this AnalyzerTestResult result, string expected) =>
DoesNotHaveDiagnostic(result?.Diagnostics, expected);
- /// Asserts that an analyzer result does not contain a diagnostic with the specified prefix.
+ ///
+ /// Asserts that an analyzer result does not contain a diagnostic with the specified prefix.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult DoesNotHaveDiagnosticThatStartsWith(
@@ -328,19 +344,25 @@ public static AssertionResult DoesNotHaveDiagnosticThatStartsWith(
string startsWithValue
) => DoesNotHaveDiagnosticThatStartsWith(result?.Diagnostics, startsWithValue);
- /// Asserts that an analyzer result does not contain error diagnostics.
+ ///
+ /// Asserts that an analyzer result does not contain error diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasNoErrorDiagnostics(this AnalyzerTestResult result) =>
HasNoErrorDiagnostics(result?.Diagnostics, "analyzer");
- /// Asserts that an analyzer result does not contain diagnostics.
+ ///
+ /// Asserts that an analyzer result does not contain diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasNoDiagnostics(this AnalyzerTestResult result) =>
HasNoDiagnostics(result?.Diagnostics, "analyzer");
- /// Asserts that a code-fix result contains the expected diagnostic.
+ ///
+ /// Asserts that a code-fix result contains the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasDiagnostic(
@@ -348,7 +370,9 @@ public static AssertionResult HasDiagnostic(
DiagnosticDescriptor expected
) => HasDiagnostic(diagnostic?.Diagnostics, expected);
- /// Asserts that a code-fix result contains the expected total number of diagnostics.
+ ///
+ /// Asserts that a code-fix result contains the expected total number of diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult> HasDiagnostics(
@@ -356,7 +380,9 @@ public static AssertionResult> HasDiagnostics(
int count
) => HasDiagnostics(diagnostic?.Diagnostics, count);
- /// Asserts that a code-fix result contains the expected number of diagnostics.
+ ///
+ /// Asserts that a code-fix result contains the expected number of diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult> HasDiagnostics(
@@ -365,13 +391,17 @@ public static AssertionResult> HasDiagnostics(
int count
) => HasDiagnostics(diagnostic?.Diagnostics, expected, count);
- /// Asserts that a code-fix result contains the expected diagnostic.
+ ///
+ /// Asserts that a code-fix result contains the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasDiagnostic(this CodeFixTestResult diagnostic, string expected) =>
HasDiagnostic(diagnostic?.Diagnostics, expected);
- /// Asserts that a code-fix result contains the expected number of diagnostics.
+ ///
+ /// Asserts that a code-fix result contains the expected number of diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult> HasDiagnostics(
@@ -380,19 +410,25 @@ public static AssertionResult> HasDiagnostics(
int count
) => HasDiagnostics(diagnostic?.Diagnostics, expected, count);
- /// Asserts that a code-fix result does not contain the expected diagnostic.
+ ///
+ /// Asserts that a code-fix result does not contain the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult DoesNotHaveDiagnostic(this CodeFixTestResult result, DiagnosticDescriptor expected) =>
DoesNotHaveDiagnostic(result?.Diagnostics, expected);
- /// Asserts that a code-fix result does not contain the expected diagnostic.
+ ///
+ /// Asserts that a code-fix result does not contain the expected diagnostic.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult DoesNotHaveDiagnostic(this CodeFixTestResult result, string expected) =>
DoesNotHaveDiagnostic(result?.Diagnostics, expected);
- /// Asserts that a code-fix result does not contain a diagnostic with the specified prefix.
+ ///
+ /// Asserts that a code-fix result does not contain a diagnostic with the specified prefix.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult DoesNotHaveDiagnosticThatStartsWith(
@@ -400,13 +436,17 @@ public static AssertionResult DoesNotHaveDiagnosticThatStartsWith(
string startsWithValue
) => DoesNotHaveDiagnosticThatStartsWith(result?.Diagnostics, startsWithValue);
- /// Asserts that a code-fix result does not contain error diagnostics.
+ ///
+ /// Asserts that a code-fix result does not contain error diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasNoErrorDiagnostics(this CodeFixTestResult result) =>
HasNoErrorDiagnostics(result?.Diagnostics, "code fix");
- /// Asserts that a code-fix result does not contain diagnostics.
+ ///
+ /// Asserts that a code-fix result does not contain diagnostics.
+ ///
[GenerateAssertion]
[EditorBrowsable(EditorBrowsableState.Never)]
public static AssertionResult HasNoDiagnostics(this CodeFixTestResult result) =>
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitCodeFixTestBase.cs b/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitCodeFixTestBase.cs
index 85ecc28..29cd7f5 100644
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitCodeFixTestBase.cs
+++ b/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitCodeFixTestBase.cs
@@ -3,13 +3,17 @@
namespace Purview.SourceGeneratorFramework.Testing.TUnit;
-/// TUnit-specific base class for analyzer and code fix tests.
+///
+/// TUnit-specific base class for analyzer and code fix tests.
+///
public abstract class TUnitCodeFixTestBase
: TUnitCodeFixTestBase
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new();
-/// TUnit-specific base class for analyzer and code fix tests.
+///
+/// TUnit-specific base class for analyzer and code fix tests.
+///
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"CA1005:Avoid excessive parameters on generic types",
@@ -22,25 +26,33 @@ public abstract class TUnitCodeFixTestBase
{
readonly CodeFixTestRunner _runner = new();
- /// Runs the analyzer and applies the selected code fix.
+ ///
+ /// Runs the analyzer and applies the selected code fix.
+ ///
protected Task ApplyCodeFixAsync(string source, CancellationToken cancellationToken = default) =>
ApplyCodeFixAsync(source, null!, cancellationToken);
- /// Runs the analyzer and applies the selected code fix using the supplied options.
+ ///
+ /// Runs the analyzer and applies the selected code fix using the supplied options.
+ ///
protected Task ApplyCodeFixAsync(
string source,
TOptions options,
CancellationToken cancellationToken = default
) => _runner.RunAsync(source, options ?? new(), cancellationToken);
- /// Runs the analyzer and applies the code fix to every diagnostic in the project.
+ ///
+ /// Runs the analyzer and applies the code fix to every diagnostic in the project.
+ ///
protected Task ApplyFixAllAsync(
IEnumerable sources,
TOptions? options = null,
CancellationToken cancellationToken = default
) => _runner.RunFixAllAsync(sources, options ?? new(), cancellationToken);
- /// Runs the analyzer and applies the code fix to every diagnostic in the project.
+ ///
+ /// Runs the analyzer and applies the code fix to every diagnostic in the project.
+ ///
protected Task ApplyFixAllAsync(
string source,
TOptions? options = null,
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitDiagnosticAnalyzerTestBase.cs b/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitDiagnosticAnalyzerTestBase.cs
index 46f4393..6b3ee1c 100644
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitDiagnosticAnalyzerTestBase.cs
+++ b/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitDiagnosticAnalyzerTestBase.cs
@@ -2,12 +2,16 @@
namespace Purview.SourceGeneratorFramework.Testing.TUnit;
-/// TUnit-specific base class for diagnostic analyzer tests.
+///
+/// TUnit-specific base class for diagnostic analyzer tests.
+///
public abstract class TUnitDiagnosticAnalyzerTestBase
: TUnitDiagnosticAnalyzerTestBase
where TAnalyzer : DiagnosticAnalyzer, new();
-/// TUnit-specific base class for diagnostic analyzer tests.
+///
+/// TUnit-specific base class for diagnostic analyzer tests.
+///
public abstract class TUnitDiagnosticAnalyzerTestBase : AnalyzerTestBase
where TAnalyzer : DiagnosticAnalyzer, new()
where TOptions : AnalyzerTestOptions, new();
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitRefactoringTestBase.cs b/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitRefactoringTestBase.cs
index d915801..4dea1db 100644
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitRefactoringTestBase.cs
+++ b/src/src/SourceGeneratorFramework.Testing.TUnit/TUnitRefactoringTestBase.cs
@@ -2,23 +2,31 @@
namespace Purview.SourceGeneratorFramework.Testing.TUnit;
-/// TUnit-specific base class for refactoring tests.
+///
+/// TUnit-specific base class for refactoring tests.
+///
public abstract class TUnitRefactoringTestBase
: TUnitRefactoringTestBase
where TRefactoring : CodeRefactoringProvider, new();
-/// TUnit-specific base class for refactoring tests.
+///
+/// TUnit-specific base class for refactoring tests.
+///
public abstract class TUnitRefactoringTestBase
where TRefactoring : CodeRefactoringProvider, new()
where TOptions : RefactorTestOptions, new()
{
readonly RefactoringTestRunner _runner = new();
- /// Runs the refactoring against the supplied source.
+ ///
+ /// Runs the refactoring against the supplied source.
+ ///
protected Task RefactorAsync(string source, CancellationToken cancellationToken = default) =>
RefactorAsync(source, null!, cancellationToken);
- /// Runs the refactoring against the supplied source using the supplied options.
+ ///
+ /// Runs the refactoring against the supplied source using the supplied options.
+ ///
protected Task RefactorAsync(
string source,
TOptions options,
diff --git a/src/src/SourceGeneratorFramework.Testing/AnalyzerTestBase.cs b/src/src/SourceGeneratorFramework.Testing/AnalyzerTestBase.cs
index 142405b..39b088b 100644
--- a/src/src/SourceGeneratorFramework.Testing/AnalyzerTestBase.cs
+++ b/src/src/SourceGeneratorFramework.Testing/AnalyzerTestBase.cs
@@ -2,12 +2,16 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// Framework-agnostic base class for diagnostic analyzer tests.
+///
+/// Framework-agnostic base class for diagnostic analyzer tests.
+///
/// The type of diagnostic analyzer.
public abstract class AnalyzerTestBase : AnalyzerTestBase
where TAnalyzer : DiagnosticAnalyzer, new();
-/// Framework-agnostic base class for diagnostic analyzer tests.
+///
+/// Framework-agnostic base class for diagnostic analyzer tests.
+///
/// The type of diagnostic analyzer.
/// The type of test options.
public abstract class AnalyzerTestBase
@@ -16,24 +20,32 @@ public abstract class AnalyzerTestBase
{
readonly DiagnosticAnalyzerTestRunner _runner = new();
- /// Runs the analyzer against the supplied source.
+ ///
+ /// Runs the analyzer against the supplied source.
+ ///
protected Task AnalyzeAsync(string source, CancellationToken cancellationToken = default) =>
AnalyzeAsync(source, null!, cancellationToken);
- /// Runs the analyzer against the supplied sources.
+ ///
+ /// Runs the analyzer against the supplied sources.
+ ///
protected Task AnalyzeAsync(
IEnumerable sources,
CancellationToken cancellationToken = default
) => AnalyzeAsync(sources, null!, cancellationToken);
- /// Runs the analyzer against the supplied source and options.
+ ///
+ /// Runs the analyzer against the supplied source and options.
+ ///
protected Task AnalyzeAsync(
string source,
TOptions options,
CancellationToken cancellationToken = default
) => AnalyzeAsync([source], options, cancellationToken);
- /// Runs the analyzer against the supplied sources and options.
+ ///
+ /// Runs the analyzer against the supplied sources and options.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Performance",
"CA1849:Call async methods when in an async method"
@@ -59,21 +71,27 @@ protected async Task AnalyzeAsync(
return result;
}
- /// Called before the analyzer is run.
+ ///
+ /// Called before the analyzer is run.
+ ///
protected virtual TOptions OnBeforeRun(
IEnumerable sources,
TOptions options,
CancellationToken cancellationToken
) => options;
- /// Called asynchronously before the analyzer is run.
+ ///
+ /// Called asynchronously before the analyzer is run.
+ ///
protected virtual Task OnBeforeRunAsync(
IEnumerable sources,
TOptions options,
CancellationToken cancellationToken
) => Task.FromResult(options);
- /// Called after the analyzer is run.
+ ///
+ /// Called after the analyzer is run.
+ ///
protected virtual void OnAfterRun(
AnalyzerTestResult result,
IEnumerable sources,
@@ -84,7 +102,9 @@ CancellationToken cancellationToken
// No-op by default.
}
- /// Called asynchronously after the analyzer is run.
+ ///
+ /// Called asynchronously after the analyzer is run.
+ ///
protected virtual Task OnAfterRunAsync(
AnalyzerTestResult result,
IEnumerable sources,
diff --git a/src/src/SourceGeneratorFramework.Testing/AnalyzerTestOptions.cs b/src/src/SourceGeneratorFramework.Testing/AnalyzerTestOptions.cs
index fdfa9f8..3eecddd 100644
--- a/src/src/SourceGeneratorFramework.Testing/AnalyzerTestOptions.cs
+++ b/src/src/SourceGeneratorFramework.Testing/AnalyzerTestOptions.cs
@@ -1,15 +1,25 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// Options that configure a diagnostic analyzer test run.
+///
+/// Options that configure a diagnostic analyzer test run.
+///
public record AnalyzerTestOptions : SourceGeneratorTestOptions;
-/// Options that configure a code fix test run.
+///
+/// Options that configure a code fix test run.
+///
public record CodeFixTestOptions : AnalyzerTestOptions
{
- /// Gets the index of the registered code action to apply.
+ ///
+ /// Gets the index of the registered code action to apply.
+ ///
public int CodeActionIndex { get; init; }
- /// Gets the equivalence key used to select a registered code action.
- /// When specified, this takes precedence over .
+ ///
+ /// Gets the equivalence key used to select a registered code action.
+ ///
+ ///
+ /// When specified, this takes precedence over .
+ ///
public string? EquivalenceKey { get; init; }
}
diff --git a/src/src/SourceGeneratorFramework.Testing/AnalyzerTestResult.cs b/src/src/SourceGeneratorFramework.Testing/AnalyzerTestResult.cs
index 0113df3..27a06a9 100644
--- a/src/src/SourceGeneratorFramework.Testing/AnalyzerTestResult.cs
+++ b/src/src/SourceGeneratorFramework.Testing/AnalyzerTestResult.cs
@@ -4,10 +4,14 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// The result of a diagnostic analyzer test run.
+///
+/// The result of a diagnostic analyzer test run.
+///
public sealed record AnalyzerTestResult(ImmutableArray Diagnostics, Compilation Compilation);
-/// The result of a code fix test run.
+///
+/// The result of a code fix test run.
+///
public sealed record CodeFixTestResult(
ImmutableArray Diagnostics,
ImmutableArray CodeActions,
@@ -16,7 +20,9 @@ public sealed record CodeFixTestResult(
Solution? ChangedSolution = null
);
-/// The result of a fix-all code fix test run.
+///
+/// The result of a fix-all code fix test run.
+///
public sealed record CodeFixFixAllResult(
ImmutableArray Diagnostics,
ImmutableArray CodeActions,
diff --git a/src/src/SourceGeneratorFramework.Testing/CodeFixTestRunner.cs b/src/src/SourceGeneratorFramework.Testing/CodeFixTestRunner.cs
index d0e78ca..96a8741 100644
--- a/src/src/SourceGeneratorFramework.Testing/CodeFixTestRunner.cs
+++ b/src/src/SourceGeneratorFramework.Testing/CodeFixTestRunner.cs
@@ -6,12 +6,16 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// Executes an analyzer and applies one code fix to a test document.
+///
+/// Executes an analyzer and applies one code fix to a test document.
+///
public sealed class CodeFixTestRunner : RoslynTestRunner
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new()
{
- /// Runs the analyzer and applies a registered code action.
+ ///
+ /// Runs the analyzer and applies a registered code action.
+ ///
public async Task RunAsync(
string source,
CodeFixTestOptions? options = null,
diff --git a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Declarations.cs b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Declarations.cs
index c4037bf..80e7cae 100644
--- a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Declarations.cs
+++ b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Declarations.cs
@@ -9,7 +9,9 @@ public sealed partial class CodeQuery
// Methods
// ---------------------------------------------------------------------------------------------
- /// Gets a method declaration by name, optionally matching its parameter types.
+ ///
+ /// Gets a method declaration by name, optionally matching its parameter types.
+ ///
/// No method matched.
public MethodDeclarationSyntax GetMethod(string name, params TypeReference[]? parameters) =>
TryGetMethod(name, out var method, parameters)
@@ -18,10 +20,14 @@ public MethodDeclarationSyntax GetMethod(string name, params TypeReference[]? pa
$"No method named '{name}' was found in the {ScopeDescription()}{(parameters is { Length: > 0 } ? " with the specified parameters" : "")}."
);
- /// Determines whether a method declaration with the given name, optionally matching parameter types, exists.
+ ///
+ /// Determines whether a method declaration with the given name, optionally matching parameter types, exists.
+ ///
public bool HasMethod(string name, params TypeReference[]? parameters) => TryGetMethod(name, out _, parameters);
- /// Attempts to get a method declaration by name, optionally matching its parameter types.
+ ///
+ /// Attempts to get a method declaration by name, optionally matching its parameter types.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
public bool TryGetMethod(string name, out MethodDeclarationSyntax? method, params TypeReference[]? parameters)
{
@@ -41,62 +47,92 @@ out method
// Type declarations
// ---------------------------------------------------------------------------------------------
- /// Gets a type declaration (class, struct, interface, record, enum or delegate) by name.
+ ///
+ /// Gets a type declaration (class, struct, interface, record, enum or delegate) by name.
+ ///
public MemberDeclarationSyntax GetTypeDeclaration(string name, string? @namespace = null) =>
Get(node => IsTypeDeclarationMatch(node, name) && NamespaceMatches(node, @namespace));
- /// Determines whether a type declaration with the given name exists.
+ ///
+ /// Determines whether a type declaration with the given name exists.
+ ///
public bool HasTypeDeclaration(string name, string? @namespace = null) =>
Has(node => IsTypeDeclarationMatch(node, name) && NamespaceMatches(node, @namespace));
- /// Gets a class declaration by name, optionally within a namespace.
+ ///
+ /// Gets a class declaration by name, optionally within a namespace.
+ ///
public ClassDeclarationSyntax GetClass(string name, string? @namespace = null) =>
FindByName(name, @namespace);
- /// Determines whether a class declaration with the given name exists, optionally within a namespace.
+ ///
+ /// Determines whether a class declaration with the given name exists, optionally within a namespace.
+ ///
public bool HasClass(string name, string? @namespace = null) => HasByName(name, @namespace);
- /// Attempts to get a class declaration by name, optionally within a namespace.
+ ///
+ /// Attempts to get a class declaration by name, optionally within a namespace.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
public bool TryGetClass(string name, out ClassDeclarationSyntax? declaration, string? @namespace = null) =>
TryFindByName(name, out declaration, @namespace);
- /// Gets a struct declaration by name, optionally within a namespace.
+ ///
+ /// Gets a struct declaration by name, optionally within a namespace.
+ ///
public StructDeclarationSyntax GetStruct(string name, string? @namespace = null) =>
FindByName(name, @namespace);
- /// Determines whether a struct declaration with the given name exists, optionally within a namespace.
+ ///
+ /// Determines whether a struct declaration with the given name exists, optionally within a namespace.
+ ///
public bool HasStruct(string name, string? @namespace = null) =>
HasByName(name, @namespace);
- /// Gets an interface declaration by name, optionally within a namespace.
+ ///
+ /// Gets an interface declaration by name, optionally within a namespace.
+ ///
public InterfaceDeclarationSyntax GetInterface(string name, string? @namespace = null) =>
FindByName(name, @namespace);
- /// Determines whether an interface declaration with the given name exists, optionally within a namespace.
+ ///
+ /// Determines whether an interface declaration with the given name exists, optionally within a namespace.
+ ///
public bool HasInterface(string name, string? @namespace = null) =>
HasByName(name, @namespace);
- /// Gets an enum declaration by name, optionally within a namespace.
+ ///
+ /// Gets an enum declaration by name, optionally within a namespace.
+ ///
public EnumDeclarationSyntax GetEnum(string name, string? @namespace = null) =>
FindByName(name, @namespace);
- /// Determines whether an enum declaration with the given name exists, optionally within a namespace.
+ ///
+ /// Determines whether an enum declaration with the given name exists, optionally within a namespace.
+ ///
public bool HasEnum(string name, string? @namespace = null) => HasByName(name, @namespace);
- /// Gets a delegate declaration by name, optionally within a namespace.
+ ///
+ /// Gets a delegate declaration by name, optionally within a namespace.
+ ///
public DelegateDeclarationSyntax GetDelegate(string name, string? @namespace = null) =>
FindByName(name, @namespace);
- /// Determines whether a delegate declaration with the given name exists, optionally within a namespace.
+ ///
+ /// Determines whether a delegate declaration with the given name exists, optionally within a namespace.
+ ///
public bool HasDelegate(string name, string? @namespace = null) =>
HasByName(name, @namespace);
- /// Gets a record declaration by name, optionally within a namespace.
+ ///
+ /// Gets a record declaration by name, optionally within a namespace.
+ ///
public RecordDeclarationSyntax GetRecord(string name, string? @namespace = null) =>
FindByName(name, @namespace);
- /// Determines whether a record declaration with the given name exists, optionally within a namespace.
+ ///
+ /// Determines whether a record declaration with the given name exists, optionally within a namespace.
+ ///
public bool HasRecord(string name, string? @namespace = null) =>
HasByName(name, @namespace);
@@ -104,30 +140,44 @@ public bool HasRecord(string name, string? @namespace = null) =>
// Members
// ---------------------------------------------------------------------------------------------
- /// Gets a property declaration by name.
+ ///
+ /// Gets a property declaration by name.
+ ///
public PropertyDeclarationSyntax GetProperty(string name) =>
TryGetProperty(name, out var property)
? property!
: throw new SyntaxNotFoundException($"No property named '{name}' was found in the {ScopeDescription()}.");
- /// Determines whether a property declaration with the given name exists.
+ ///
+ /// Determines whether a property declaration with the given name exists.
+ ///
public bool HasProperty(string name) => TryGetProperty(name, out _);
- /// Attempts to get a property declaration by name.
+ ///
+ /// Attempts to get a property declaration by name.
+ ///
public bool TryGetProperty(string name, out PropertyDeclarationSyntax? property) =>
TryFindByName(name, out property);
- /// Gets a field declaration by name.
- /// Finds a by identifier and returns its declaring field.
+ ///
+ /// Gets a field declaration by name.
+ ///
+ ///
+ /// Finds a by identifier and returns its declaring field.
+ ///
public FieldDeclarationSyntax GetField(string name) =>
TryGetField(name, out var field)
? field!
: throw new SyntaxNotFoundException($"No field named '{name}' was found in the {ScopeDescription()}.");
- /// Determines whether a field declaration with the given name exists.
+ ///
+ /// Determines whether a field declaration with the given name exists.
+ ///
public bool HasField(string name) => TryGetField(name, out _);
- /// Attempts to get a field declaration by name.
+ ///
+ /// Attempts to get a field declaration by name.
+ ///
public bool TryGetField(string name, out FieldDeclarationSyntax? field)
{
if (string.IsNullOrWhiteSpace(name))
@@ -135,7 +185,7 @@ public bool TryGetField(string name, out FieldDeclarationSyntax? field)
foreach (var tree in Trees)
{
- foreach (var declarator in tree.GetRoot().DescendantNodes().OfType())
+ foreach (var declarator in RootOf(tree).DescendantNodes().OfType())
{
if (declarator.Identifier.ValueText != name)
continue;
@@ -152,15 +202,21 @@ public bool TryGetField(string name, out FieldDeclarationSyntax? field)
return false;
}
- /// Gets a constructor declaration by the name of its containing type.
+ ///
+ /// Gets a constructor declaration by the name of its containing type.
+ ///
public ConstructorDeclarationSyntax GetConstructor(string containingTypeName) =>
FindByName(containingTypeName);
- /// Determines whether a constructor declaration for the given containing type exists.
+ ///
+ /// Determines whether a constructor declaration for the given containing type exists.
+ ///
public bool HasConstructor(string containingTypeName) =>
HasByName(containingTypeName);
- /// Gets a namespace declaration (block or file-scoped) by its dotted name.
+ ///
+ /// Gets a namespace declaration (block or file-scoped) by its dotted name.
+ ///
public BaseNamespaceDeclarationSyntax GetNamespace(string name) =>
FindByName(
name,
@@ -168,7 +224,9 @@ public BaseNamespaceDeclarationSyntax GetNamespace(string name) =>
namespaceDeclaration => namespaceDeclaration.Name.ToString()
);
- /// Determines whether a namespace declaration with the given dotted name exists.
+ ///
+ /// Determines whether a namespace declaration with the given dotted name exists.
+ ///
public bool HasNamespace(string name) =>
HasByName(
name,
@@ -227,7 +285,7 @@ bool TryFind(string name, Func getName, Func? additional,
{
foreach (var tree in Trees)
{
- foreach (var candidate in tree.GetRoot().DescendantNodes().OfType())
+ foreach (var candidate in RootOf(tree).DescendantNodes().OfType())
{
if (getName(candidate) != name)
continue;
diff --git a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Members.cs b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Members.cs
new file mode 100644
index 0000000..6c7ba95
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Members.cs
@@ -0,0 +1,226 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Purview.SourceGeneratorFramework.Testing;
+
+public sealed partial class CodeQuery
+{
+ // ---------------------------------------------------------------------------------------------
+ // Operators
+ // ---------------------------------------------------------------------------------------------
+
+ ///
+ /// Gets the first operator declaration matching the given token, such as == or implicit.
+ ///
+ /// No operator matched.
+ public OperatorDeclarationSyntax GetOperator(string operatorToken) =>
+ TryGetOperator(operatorToken, out var @operator)
+ ? @operator!
+ : throw new SyntaxNotFoundException(
+ $"No operator '{operatorToken}' was found in the {ScopeDescription()}."
+ );
+
+ ///
+ /// Determines whether an operator declaration with the given token exists.
+ ///
+ public bool HasOperator(string operatorToken) => TryGetOperator(operatorToken, out _);
+
+ ///
+ /// Attempts to get the first operator declaration matching the given token.
+ ///
+ public bool TryGetOperator(string operatorToken, out OperatorDeclarationSyntax? @operator)
+ {
+ if (string.IsNullOrWhiteSpace(operatorToken))
+ throw new ArgumentException("The operator token cannot be null or whitespace.", nameof(operatorToken));
+
+ foreach (var tree in Trees)
+ {
+ foreach (var candidate in RootOf(tree).DescendantNodes().OfType())
+ {
+ if (candidate.OperatorToken.ValueText == operatorToken)
+ {
+ @operator = candidate;
+ return true;
+ }
+ }
+ }
+
+ @operator = null;
+ return false;
+ }
+
+ ///
+ /// Gets the first conversion operator matching the given keyword, such as implicit or explicit.
+ ///
+ /// No conversion operator matched.
+ public ConversionOperatorDeclarationSyntax GetConversionOperator(string keyword) =>
+ TryGetConversionOperator(keyword, out var conversion)
+ ? conversion!
+ : throw new SyntaxNotFoundException(
+ $"No '{keyword}' conversion operator was found in the {ScopeDescription()}."
+ );
+
+ ///
+ /// Determines whether a conversion operator with the given keyword exists.
+ ///
+ public bool HasConversionOperator(string keyword) => TryGetConversionOperator(keyword, out _);
+
+ ///
+ /// Attempts to get the first conversion operator matching the given keyword.
+ ///
+ public bool TryGetConversionOperator(string keyword, out ConversionOperatorDeclarationSyntax? conversion)
+ {
+ if (keyword is not ("implicit" or "explicit"))
+ throw new ArgumentException("The keyword must be 'implicit' or 'explicit'.", nameof(keyword));
+
+ foreach (var tree in Trees)
+ {
+ foreach (var candidate in RootOf(tree).DescendantNodes().OfType())
+ {
+ if (candidate.ImplicitOrExplicitKeyword.ValueText == keyword)
+ {
+ conversion = candidate;
+ return true;
+ }
+ }
+ }
+
+ conversion = null;
+ return false;
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // Indexers
+ // ---------------------------------------------------------------------------------------------
+
+ ///
+ /// Gets an indexer declaration whose parameters match the given types.
+ ///
+ /// No indexer matched.
+ public IndexerDeclarationSyntax GetIndexer(params TypeReference[] parameters) =>
+ TryGetIndexer(out var indexer, parameters)
+ ? indexer!
+ : throw new SyntaxNotFoundException($"No indexer was found in the {ScopeDescription()}.");
+
+ ///
+ /// Determines whether an indexer declaration with the given parameter types exists.
+ ///
+ public bool HasIndexer(params TypeReference[] parameters) => TryGetIndexer(out _, parameters);
+
+ ///
+ /// Attempts to get an indexer declaration whose parameters match the given types.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
+ public bool TryGetIndexer(out IndexerDeclarationSyntax? indexer, params TypeReference[] parameters)
+ {
+ var expected = parameters ?? [];
+ foreach (var tree in Trees)
+ {
+ foreach (var candidate in RootOf(tree).DescendantNodes().OfType())
+ {
+ if (expected.Length == 0 || MatchesParameters(candidate.ParameterList, expected))
+ {
+ indexer = candidate;
+ return true;
+ }
+ }
+ }
+
+ indexer = null;
+ return false;
+ }
+
+ bool MatchesParameters(BracketedParameterListSyntax parameterList, TypeReference[] expected)
+ {
+ var parameters = parameterList.Parameters;
+ if (parameters.Count != expected.Length)
+ return false;
+
+ for (var index = 0; index < parameters.Count; index++)
+ {
+ var typeSyntax = parameters[index].Type;
+ if (typeSyntax is null || !Matches(typeSyntax, expected[index]))
+ return false;
+ }
+
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // Attributes
+ // ---------------------------------------------------------------------------------------------
+
+ ///
+ /// Gets all attribute applications on or within the given node.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
+ public ImmutableArray GetAttributes(SyntaxNode node)
+ {
+ if (node is null)
+ throw new ArgumentNullException(nameof(node));
+
+ // Note: This intentionally returns attributes on the node itself and any nested nodes, such as parameters.
+ return [.. node.DescendantNodes().OfType()];
+ }
+
+ ///
+ /// Determines whether the given node has an attribute with the specified name.
+ ///
+ ///
+ /// The name may be supplied with or without the Attribute suffix.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
+ public bool HasAttribute(SyntaxNode node, string name)
+ {
+ if (node is null)
+ throw new ArgumentNullException(nameof(node));
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("The attribute name cannot be null or whitespace.", nameof(name));
+
+ foreach (var attribute in node.DescendantNodes().OfType())
+ {
+ if (MatchesAttributeName(attribute, name))
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Gets the first attribute with the specified name, or .
+ ///
+ ///
+ /// The name may be supplied with or without the Attribute suffix.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
+ public AttributeSyntax? GetAttribute(SyntaxNode node, string name)
+ {
+ if (node is null)
+ throw new ArgumentNullException(nameof(node));
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("The attribute name cannot be null or whitespace.", nameof(name));
+
+ foreach (var attribute in node.DescendantNodes().OfType())
+ {
+ if (MatchesAttributeName(attribute, name))
+ return attribute;
+ }
+
+ return null;
+ }
+
+ static bool MatchesAttributeName(AttributeSyntax attribute, string name)
+ {
+ var rendered = attribute.Name.ToString();
+ if (string.Equals(rendered, name, StringComparison.Ordinal))
+ return true;
+
+ // The attribute name may be supplied with or without the "Attribute" suffix, and may be fully qualified.
+ return name.EndsWith("Attribute", StringComparison.Ordinal)
+ ? string.Equals(rendered, name, StringComparison.Ordinal)
+ : string.Equals(rendered, name + "Attribute", StringComparison.Ordinal)
+ || string.Equals(rendered, $"global::{name}", StringComparison.Ordinal)
+ || string.Equals(rendered, $"global::{name}Attribute", StringComparison.Ordinal);
+ }
+}
diff --git a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Signatures.cs b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Signatures.cs
index 01b6f37..7195e5c 100644
--- a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Signatures.cs
+++ b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Signatures.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Purview.SourceGeneratorFramework.Testing;
@@ -34,7 +35,9 @@ public bool HasParameters(BaseMethodDeclarationSyntax method, params TypeReferen
return true;
}
- /// Determines whether a method declaration's return type matches the given reference.
+ ///
+ /// Determines whether a method declaration's return type matches the given reference.
+ ///
public bool HasReturnType(string methodName, TypeReference returnType)
{
if (string.IsNullOrWhiteSpace(methodName))
@@ -79,6 +82,21 @@ public bool Matches(TypeSyntax typeSyntax, TypeReference reference)
[EditorBrowsable(EditorBrowsableState.Never)]
public static class CodeQuerySignatureExtensions
{
+ ///
+ /// Creates a nested query scoped to this node, enabling chained searches such as
+ /// query.GetClass("C").Query(query).GetMethod("M").
+ ///
+ public static CodeQuery Query(this SyntaxNode node, CodeQuery parent)
+ {
+ if (node is null)
+ throw new ArgumentNullException(nameof(node));
+ if (parent is null)
+ throw new ArgumentNullException(nameof(parent));
+
+ // Delegate to the parent query's In method, which creates a new query scoped to the given node.
+ return parent.In(node);
+ }
+
///
/// Determines whether the method's or constructor's parameters match the given types, resolved through the
/// query's compilation.
diff --git a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Statements.cs b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Statements.cs
new file mode 100644
index 0000000..88b6205
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Statements.cs
@@ -0,0 +1,139 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Purview.SourceGeneratorFramework.Testing;
+
+public sealed partial class CodeQuery
+{
+ // ---------------------------------------------------------------------------------------------
+ // Statements
+ // ---------------------------------------------------------------------------------------------
+
+ ///
+ /// Gets the first foreach statement, optionally matching its iterator text.
+ ///
+ /// No statement matched.
+ public ForEachStatementSyntax GetForeach(string? iterator = null) =>
+ Get(statement => iterator is null || statement.Expression.ToString() == iterator);
+
+ ///
+ /// Determines whether a foreach statement exists, optionally matching its iterator text.
+ ///
+ public bool HasForeach(string? iterator = null) =>
+ Has(statement => iterator is null || statement.Expression.ToString() == iterator);
+
+ ///
+ /// Gets the first for statement.
+ ///
+ /// No statement matched.
+ public ForStatementSyntax GetFor() => Get();
+
+ ///
+ /// Determines whether a for statement exists.
+ ///
+ public bool HasFor() => Has();
+
+ ///
+ /// Gets the first while statement, optionally matching its condition.
+ ///
+ /// No statement matched.
+ public WhileStatementSyntax GetWhile(string? condition = null) =>
+ Get(statement => condition is null || statement.Condition.ToString() == condition);
+
+ ///
+ /// Determines whether a while statement exists, optionally matching its condition.
+ ///
+ public bool HasWhile(string? condition = null) =>
+ Has(statement => condition is null || statement.Condition.ToString() == condition);
+
+ ///
+ /// Gets the first if statement.
+ ///
+ /// No statement matched.
+ public IfStatementSyntax GetIf() => Get();
+
+ ///
+ /// Determines whether an if statement exists.
+ ///
+ public bool HasIf() => Has();
+
+ ///
+ /// Gets the first try statement.
+ ///
+ /// No statement matched.
+ public TryStatementSyntax GetTry() => Get();
+
+ ///
+ /// Determines whether a try statement exists.
+ ///
+ public bool HasTry() => Has();
+
+ ///
+ /// Gets the first invocation of a method with the given simple name.
+ ///
+ /// No invocation matched.
+ public InvocationExpressionSyntax GetInvocation(string methodName) =>
+ TryGetInvocation(methodName, out var invocation)
+ ? invocation!
+ : throw new SyntaxNotFoundException(
+ $"No invocation of '{methodName}' was found in the {ScopeDescription()}."
+ );
+
+ ///
+ /// Determines whether an invocation of a method with the given simple name exists.
+ ///
+ public bool HasInvocation(string methodName) => TryGetInvocation(methodName, out _);
+
+ ///
+ /// Attempts to get the first invocation of a method with the given simple name.
+ ///
+ public bool TryGetInvocation(string methodName, out InvocationExpressionSyntax? invocation)
+ {
+ if (string.IsNullOrWhiteSpace(methodName))
+ throw new ArgumentException("The method name cannot be null or whitespace.", nameof(methodName));
+
+ foreach (var tree in Trees)
+ {
+ foreach (var candidate in RootOf(tree).DescendantNodes().OfType())
+ {
+ if (GetInvokedMethodName(candidate) == methodName)
+ {
+ invocation = candidate;
+ return true;
+ }
+ }
+ }
+
+ invocation = null;
+ return false;
+ }
+
+ ///
+ /// Determines whether an object-creation expression of the given type exists.
+ ///
+ public bool HasObjectCreation(TypeReference type)
+ {
+ if (type is null)
+ throw new ArgumentNullException(nameof(type));
+
+ foreach (var tree in Trees)
+ {
+ foreach (var candidate in RootOf(tree).DescendantNodes().OfType())
+ {
+ if (Matches(candidate.Type, type))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static string? GetInvokedMethodName(InvocationExpressionSyntax invocation) =>
+ invocation.Expression switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
+ GenericNameSyntax generic => generic.Identifier.ValueText,
+ MemberAccessExpressionSyntax member => member.Name.Identifier.ValueText,
+ MemberBindingExpressionSyntax binding => binding.Name.Identifier.ValueText,
+ _ => null,
+ };
+}
diff --git a/src/src/SourceGeneratorFramework.Testing/CodeQuery.cs b/src/src/SourceGeneratorFramework.Testing/CodeQuery.cs
index 698f5a2..3c6c0b6 100644
--- a/src/src/SourceGeneratorFramework.Testing/CodeQuery.cs
+++ b/src/src/SourceGeneratorFramework.Testing/CodeQuery.cs
@@ -12,33 +12,70 @@ namespace Purview.SourceGeneratorFramework.Testing;
/// Every operation is synchronous: SyntaxTree.GetRoot() and
/// Compilation.GetSemanticModel are lazy, so repeated queries over test-sized payloads are cheap.
///
-/// Initializes a new query over the given trees.
+///
+/// Initializes a new query over the given trees.
+///
/// The trees to search.
///
/// The compilation backing , used to resolve symbols for type matching. May be
/// when only syntactic matching is required.
///
/// Whether the trees represent generated code, used in error messages.
+///
+/// An optional node that scopes every node search to its subtree, enabling nested queries such as
+/// query.In(query.GetClass("C")).GetMethod("M").
+///
public sealed partial class CodeQuery(
ImmutableArray trees,
Compilation? compilation = null,
- bool isGenerated = false
+ bool isGenerated = false,
+ SyntaxNode? root = null
)
{
- /// Gets the trees being searched.
+ ///
+ /// Gets the trees being searched.
+ ///
public ImmutableArray Trees { get; } = trees.IsDefault ? [] : trees;
- /// Gets the compilation backing the trees, when one is available.
+ ///
+ /// Gets the compilation backing the trees, when one is available.
+ ///
public Compilation? Compilation { get; } = compilation;
- /// Gets whether the trees represent generated code, used in error messages.
+ ///
+ /// Gets whether the trees represent generated code, used in error messages.
+ ///
public bool IsGenerated { get; } = isGenerated;
+ ///
+ /// Gets the node that scopes node searches, when this is a nested query.
+ ///
+ public SyntaxNode? Root { get; } = root;
+
+ ///
+ /// Creates a nested query scoped to , sharing this query's trees and compilation.
+ ///
+ /// The node whose subtree is searched.
+ /// A child query scoped to the node.
+ /// query.In(query.GetClass("OrderAggregate")).HasMethod("Handle", orderCreated);
+ public CodeQuery In(SyntaxNode root)
+ {
+ if (root is null)
+ throw new ArgumentNullException(nameof(root));
+
+ // Delegate to the constructor, which validates that the root is in one of the query's trees.
+ return new(Trees, Compilation, IsGenerated, root);
+ }
+
+ SyntaxNode RootOf(SyntaxTree tree) => Root ?? tree.GetRoot();
+
// ---------------------------------------------------------------------------------------------
// Syntax trees
// ---------------------------------------------------------------------------------------------
- /// Gets the tree whose file path ends with or equals the given name.
+ ///
+ /// Gets the tree whose file path ends with or equals the given name.
+ ///
/// No tree matched.
public SyntaxTree GetSyntaxTree(string name) =>
TryGetSyntaxTree(name, out var tree)
@@ -47,10 +84,14 @@ public SyntaxTree GetSyntaxTree(string name) =>
$"No syntax tree named '{name}' was found in the {ScopeDescription()}."
);
- /// Determines whether a tree whose file path ends with or equals the given name exists.
+ ///
+ /// Determines whether a tree whose file path ends with or equals the given name exists.
+ ///
public bool HasSyntaxTree(string name) => TryGetSyntaxTree(name, out _);
- /// Attempts to get the tree whose file path ends with or equals the given name.
+ ///
+ /// Attempts to get the tree whose file path ends with or equals the given name.
+ ///
public bool TryGetSyntaxTree(string name, out SyntaxTree? tree)
{
if (string.IsNullOrWhiteSpace(name))
@@ -76,23 +117,71 @@ public bool TryGetSyntaxTree(string name, out SyntaxTree? tree)
// Generic
// ---------------------------------------------------------------------------------------------
- /// Gets the first syntax node of the specified type, optionally matching a predicate.
+ ///
+ /// Gets the first syntax node of the specified type, optionally matching a predicate.
+ ///
/// No node matched.
public T Get(Func? predicate = null)
where T : SyntaxNode => TryGet(out var node, predicate) ? node! : throw NotFound();
- /// Determines whether a syntax node of the specified type exists, optionally matching a predicate.
+ ///
+ /// Determines whether a syntax node of the specified type exists, optionally matching a predicate.
+ ///
public bool Has(Func? predicate = null)
where T : SyntaxNode => TryGet(out _, predicate);
- /// Attempts to get the first syntax node of the specified type, optionally matching a predicate.
+ ///
+ /// Gets all syntax nodes of the specified type, optionally matching a predicate.
+ ///
+ public ImmutableArray GetAll(Func? predicate = null)
+ where T : SyntaxNode
+ {
+ var builder = ImmutableArray.CreateBuilder();
+ foreach (var tree in Trees)
+ {
+ var root = RootOf(tree);
+ if (root is T rootNode && (predicate is null || predicate(rootNode)))
+ builder.Add(rootNode);
+
+ foreach (var candidate in root.DescendantNodes().OfType())
+ {
+ if (predicate is null || predicate(candidate))
+ builder.Add(candidate);
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
+ ///
+ /// Counts the syntax nodes of the specified type, optionally matching a predicate.
+ ///
+ public int Count(Func? predicate = null)
+ where T : SyntaxNode
+ {
+ var count = 0;
+ foreach (var tree in Trees)
+ {
+ var root = RootOf(tree);
+ if (root is T rootNode && (predicate is null || predicate(rootNode)))
+ count++;
+
+ count += root.DescendantNodes().OfType().Count(candidate => predicate is null || predicate(candidate));
+ }
+
+ return count;
+ }
+
+ ///
+ /// Attempts to get the first syntax node of the specified type, optionally matching a predicate.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
public bool TryGet(out T? node, Func? predicate = null)
where T : SyntaxNode
{
foreach (var tree in Trees)
{
- var root = tree.GetRoot();
+ var root = RootOf(tree);
if (root is T rootNode && (predicate is null || predicate(rootNode)))
{
node = rootNode;
diff --git a/src/src/SourceGeneratorFramework.Testing/CodeQueryResultExtensions.cs b/src/src/SourceGeneratorFramework.Testing/CodeQueryResultExtensions.cs
index 80c01dd..04e0033 100644
--- a/src/src/SourceGeneratorFramework.Testing/CodeQueryResultExtensions.cs
+++ b/src/src/SourceGeneratorFramework.Testing/CodeQueryResultExtensions.cs
@@ -11,7 +11,9 @@ namespace Purview.SourceGeneratorFramework.Testing;
///
public static class CodeQueryResultExtensions
{
- /// Gets a query over the generated trees of a source-generator run, with the output compilation.
+ ///
+ /// Gets a query over the generated trees of a source-generator run, with the output compilation.
+ ///
public static CodeQuery Generated(this DriverRunResult result)
{
if (result is null)
@@ -21,7 +23,9 @@ public static CodeQuery Generated(this DriverRunResult result)
return new(result.AllSyntaxTrees, compilation, isGenerated: true);
}
- /// Gets a query over the entire output compilation (user and generated trees) of a source-generator run.
+ ///
+ /// Gets a query over the entire output compilation (user and generated trees) of a source-generator run.
+ ///
public static CodeQuery Output(this DriverRunResult result)
{
if (result is null)
@@ -31,7 +35,9 @@ public static CodeQuery Output(this DriverRunResult result)
return new([.. compilation.SyntaxTrees], compilation);
}
- /// Gets a query over the trees of an analyzer test compilation.
+ ///
+ /// Gets a query over the trees of an analyzer test compilation.
+ ///
public static CodeQuery Code(this AnalyzerTestResult result)
{
if (result is null)
@@ -41,7 +47,9 @@ public static CodeQuery Code(this AnalyzerTestResult result)
return new([.. result.Compilation.SyntaxTrees], result.Compilation);
}
- /// Gets a query over the input compilation of a code-fix test.
+ ///
+ /// Gets a query over the input compilation of a code-fix test.
+ ///
public static CodeQuery Code(this CodeFixTestResult result)
{
if (result is null)
@@ -51,7 +59,9 @@ public static CodeQuery Code(this CodeFixTestResult result)
return new([.. result.Compilation.SyntaxTrees], result.Compilation);
}
- /// Gets a query over the fixed source produced by a code-fix test.
+ ///
+ /// Gets a query over the fixed source produced by a code-fix test.
+ ///
public static CodeQuery FixedCode(this CodeFixTestResult result)
{
if (result is null)
@@ -67,7 +77,9 @@ public static CodeQuery FixedCode(this CodeFixTestResult result)
return new(ParseSource(result.FixedSource), null);
}
- /// Gets a query over the fixed sources produced by a fix-all code-fix test.
+ ///
+ /// Gets a query over the fixed sources produced by a fix-all code-fix test.
+ ///
public static CodeQuery FixedCode(this CodeFixFixAllResult result)
{
if (result is null)
@@ -78,7 +90,9 @@ public static CodeQuery FixedCode(this CodeFixFixAllResult result)
return compilation is null ? new([], null) : new([.. compilation.SyntaxTrees], compilation);
}
- /// Gets a query over the refactored sources produced by a refactoring test.
+ ///
+ /// Gets a query over the refactored sources produced by a refactoring test.
+ ///
public static CodeQuery FixedCode(this RefactorTestResult result)
{
if (result is null)
diff --git a/src/src/SourceGeneratorFramework.Testing/DiagnosticAnalyzerTestRunner.cs b/src/src/SourceGeneratorFramework.Testing/DiagnosticAnalyzerTestRunner.cs
index 80f075d..c153c42 100644
--- a/src/src/SourceGeneratorFramework.Testing/DiagnosticAnalyzerTestRunner.cs
+++ b/src/src/SourceGeneratorFramework.Testing/DiagnosticAnalyzerTestRunner.cs
@@ -2,18 +2,24 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// Executes a diagnostic analyzer against a test compilation.
+///
+/// Executes a diagnostic analyzer against a test compilation.
+///
public sealed class DiagnosticAnalyzerTestRunner : RoslynTestRunner
where TAnalyzer : DiagnosticAnalyzer, new()
{
- /// Runs the analyzer against one source file.
+ ///
+ /// Runs the analyzer against one source file.
+ ///
public Task RunAsync(
string source,
AnalyzerTestOptions? options = null,
CancellationToken cancellationToken = default
) => RunAsync([source], options, cancellationToken);
- /// Runs the analyzer against the supplied source files.
+ ///
+ /// Runs the analyzer against the supplied source files.
+ ///
public async Task RunAsync(
IEnumerable sources,
AnalyzerTestOptions? options = null,
diff --git a/src/src/SourceGeneratorFramework.Testing/DriverRunResult.cs b/src/src/SourceGeneratorFramework.Testing/DriverRunResult.cs
index c257eb8..8ff0a4b 100644
--- a/src/src/SourceGeneratorFramework.Testing/DriverRunResult.cs
+++ b/src/src/SourceGeneratorFramework.Testing/DriverRunResult.cs
@@ -85,7 +85,9 @@ static string GetDiagnosticKey(Diagnostic diagnostic) =>
/// The mode to use when matching the hint name.
/// The source text of the generated tree, or if not found.
/// Thrown if is or whitespace.
- /// The is matched using .
+ ///
+ /// The is matched using .
+ ///
public string? GetSource(string hintName, HintNameMatchMode matchMode = HintNameMatchMode.Suffix)
{
if (string.IsNullOrWhiteSpace(hintName))
@@ -185,7 +187,9 @@ public enum HintNameMatchMode
///
/// Match the hint name by suffix.
///
- /// Note this will automatically check for .cs if it's excluded.
+ ///
+ /// Note this will automatically check for .cs if it's excluded.
+ ///
Suffix,
///
diff --git a/src/src/SourceGeneratorFramework.Testing/DriverRunValidationException.cs b/src/src/SourceGeneratorFramework.Testing/DriverRunValidationException.cs
index 0e9cb14..12aa3f5 100644
--- a/src/src/SourceGeneratorFramework.Testing/DriverRunValidationException.cs
+++ b/src/src/SourceGeneratorFramework.Testing/DriverRunValidationException.cs
@@ -61,19 +61,29 @@ ImmutableArray compilationTrees
LogErrors = logErrors;
}
- /// Gets the result of the source generator test run that failed validation.
+ ///
+ /// Gets the result of the source generator test run that failed validation.
+ ///
public DriverRunResult RunResult { get; }
- /// Gets exceptions thrown by generators.
+ ///
+ /// Gets exceptions thrown by generators.
+ ///
public IReadOnlyList GeneratorFailures { get; }
- /// Gets errors reported by the output compilation.
+ ///
+ /// Gets errors reported by the output compilation.
+ ///
public IReadOnlyList CompilationErrors { get; }
- /// Gets additional errors reported while emitting the output assembly.
+ ///
+ /// Gets additional errors reported while emitting the output assembly.
+ ///
public IReadOnlyList EmitErrors { get; }
- /// Gets error-level entries written through generator logging.
+ ///
+ /// Gets error-level entries written through generator logging.
+ ///
public IReadOnlyList LogErrors { get; }
static string BuildMessage(
diff --git a/src/src/SourceGeneratorFramework.Testing/Extensions/Purview/SourceGeneratorFramework/SourceGeneratorTestOptionsExtensions.cs b/src/src/SourceGeneratorFramework.Testing/Extensions/Purview/SourceGeneratorFramework/SourceGeneratorTestOptionsExtensions.cs
index 2de32fc..d17d83e 100644
--- a/src/src/SourceGeneratorFramework.Testing/Extensions/Purview/SourceGeneratorFramework/SourceGeneratorTestOptionsExtensions.cs
+++ b/src/src/SourceGeneratorFramework.Testing/Extensions/Purview/SourceGeneratorFramework/SourceGeneratorTestOptionsExtensions.cs
@@ -106,7 +106,9 @@ options with
}
);
- /// Creates a new options snapshot with additional namespaces appended.
+ ///
+ /// Creates a new options snapshot with additional namespaces appended.
+ ///
public TOptions WithExcludeGeneratedSourceHintNames(IEnumerable sourceHintNames) =>
sourceHintNames is null
? options
@@ -119,19 +121,25 @@ options with
}
);
- /// Creates a new options snapshot with additional namespaces appended.
+ ///
+ /// Creates a new options snapshot with additional namespaces appended.
+ ///
public TOptions WithAdditionalNamespaces(params string[] additionalNamespaces) =>
additionalNamespaces is null || additionalNamespaces.Length == 0
? options
: (options with { AdditionalNamespaces = options.AdditionalNamespaces.AddRange(additionalNamespaces) });
- /// Creates a new options snapshot with additional namespaces appended.
+ ///
+ /// Creates a new options snapshot with additional namespaces appended.
+ ///
public TOptions WithAdditionalNamespaces(IEnumerable additionalNamespaces) =>
additionalNamespaces is null
? options
: (options with { AdditionalNamespaces = options.AdditionalNamespaces.AddRange(additionalNamespaces) });
- /// Creates a new options snapshot with additional namespaces appended.
+ ///
+ /// Creates a new options snapshot with additional namespaces appended.
+ ///
public TOptions WithAdditionalNamespaces(params TypeIdentity[] identities) =>
identities is null || identities.Length == 0
? options
@@ -144,7 +152,9 @@ options with
}
);
- /// Creates a new options snapshot with additional namespaces appended.
+ ///
+ /// Creates a new options snapshot with additional namespaces appended.
+ ///
public TOptions WithAdditionalNamespaces(IEnumerable identities) =>
identities is null
? options
@@ -157,31 +167,41 @@ options with
}
);
- /// Creates a new options snapshot with additional metadata references appended.
+ ///
+ /// Creates a new options snapshot with additional metadata references appended.
+ ///
public TOptions WithAdditionalReferences(params MetadataReference[] additionalReferences) =>
additionalReferences is null || additionalReferences.Length == 0
? options
: (options with { AdditionalReferences = options.AdditionalReferences.AddRange(additionalReferences) });
- /// Creates a new options snapshot with additional metadata references appended.
+ ///
+ /// Creates a new options snapshot with additional metadata references appended.
+ ///
public TOptions WithAdditionalReferences(IEnumerable additionalReferences) =>
additionalReferences is null || !additionalReferences.Any()
? options
: (options with { AdditionalReferences = options.AdditionalReferences.AddRange(additionalReferences) });
- /// Creates a new options snapshot with additional source files appended.
+ ///
+ /// Creates a new options snapshot with additional source files appended.
+ ///
public TOptions WithAdditionalSources(params string[] additionalSources) =>
additionalSources is null || additionalSources.Length == 0
? options
: (options with { AdditionalSources = options.AdditionalSources.AddRange(additionalSources) });
- /// Creates a new options snapshot with additional source files appended.
+ ///
+ /// Creates a new options snapshot with additional source files appended.
+ ///
public TOptions WithAdditionalSources(IEnumerable additionalSources) =>
additionalSources is null || !additionalSources.Any()
? options
: (options with { AdditionalSources = options.AdditionalSources.AddRange(additionalSources) });
- /// Creates a new options snapshot with additional source files appended.
+ ///
+ /// Creates a new options snapshot with additional source files appended.
+ ///
public TOptions WithAdditionalSources(params SourceText[] additionalSources) =>
additionalSources is null || additionalSources.Length == 0
? options
@@ -194,7 +214,9 @@ options with
}
);
- /// Creates a new options snapshot with additional source files appended.
+ ///
+ /// Creates a new options snapshot with additional source files appended.
+ ///
public TOptions WithAdditionalSources(IEnumerable additionalSources) =>
additionalSources is null
? options
@@ -207,19 +229,25 @@ options with
}
);
- /// Creates a new options snapshot with additional text files appended.
+ ///
+ /// Creates a new options snapshot with additional text files appended.
+ ///
public TOptions WithAdditionalText(params AdditionalText[] additionalText) =>
additionalText is null || additionalText.Length == 0
? options
: (options with { AdditionalText = options.AdditionalText.AddRange(additionalText) });
- /// Creates a new options snapshot with additional text files appended.
+ ///
+ /// Creates a new options snapshot with additional text files appended.
+ ///
public TOptions WithAdditionalText(IEnumerable additionalText) =>
additionalText is null
? options
: (options with { AdditionalText = options.AdditionalText.AddRange(additionalText) });
- /// Creates a new options snapshot with analyzer types appended.
+ ///
+ /// Creates a new options snapshot with analyzer types appended.
+ ///
public TOptions WithAnalyzers(params Type[] analyzerTypes)
{
if (analyzerTypes is null || analyzerTypes.Length == 0)
@@ -242,7 +270,9 @@ public TOptions WithAnalyzers(params Type[] analyzerTypes)
};
}
- /// Creates a new options snapshot with analyzer types appended.
+ ///
+ /// Creates a new options snapshot with analyzer types appended.
+ ///
public TOptions WithAnalyzers(IEnumerable analyzerTypes)
{
if (analyzerTypes is null)
@@ -265,8 +295,12 @@ public TOptions WithAnalyzers(IEnumerable analyzerTypes)
};
}
- /// Creates a new options snapshot using the specified analyzer options.
- /// This clears .
+ ///
+ /// Creates a new options snapshot using the specified analyzer options.
+ ///
+ ///
+ /// This clears .
+ ///
public TOptions WithAnalyzerOptions(AnalyzerOptions? analyzerOptions) =>
options with
{
@@ -274,8 +308,12 @@ options with
CompilationWithAnalyzersOptions = null,
};
- /// Creates a new options snapshot using the specified compilation-with-analyzers options.
- /// This clears .
+ ///
+ /// Creates a new options snapshot using the specified compilation-with-analyzers options.
+ ///
+ ///
+ /// This clears .
+ ///
public TOptions WithCompilationWithAnalyzersOptions(
CompilationWithAnalyzersOptions? compilationWithAnalyzersOptions
) => options with { AnalyzerOptions = null, CompilationWithAnalyzersOptions = compilationWithAnalyzersOptions };
@@ -284,8 +322,12 @@ public TOptions WithCompilationWithAnalyzersOptions(
extension(TOptions options)
where TOptions : CodeFixTestOptions
{
- /// Creates a new code-fix options snapshot selecting a code action by index.
- /// This clears .
+ ///
+ /// Creates a new code-fix options snapshot selecting a code action by index.
+ ///
+ ///
+ /// This clears .
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Usage",
"CA1512:Use ArgumentOutOfRangeException throw helper",
@@ -296,7 +338,9 @@ public TOptions WithCodeActionIndex(int codeActionIndex) =>
? throw new ArgumentOutOfRangeException(nameof(codeActionIndex))
: (options with { CodeActionIndex = codeActionIndex, EquivalenceKey = null });
- /// Creates a new code-fix options snapshot selecting a code action by equivalence key.
+ ///
+ /// Creates a new code-fix options snapshot selecting a code action by equivalence key.
+ ///
public TOptions WithCodeActionEquivalenceKey(string equivalenceKey) =>
string.IsNullOrWhiteSpace(equivalenceKey)
? throw new ArgumentException("Value cannot be null or whitespace.", nameof(equivalenceKey))
diff --git a/src/src/SourceGeneratorFramework.Testing/MemberQueryExtensions.cs b/src/src/SourceGeneratorFramework.Testing/MemberQueryExtensions.cs
index a79aa75..893da63 100644
--- a/src/src/SourceGeneratorFramework.Testing/MemberQueryExtensions.cs
+++ b/src/src/SourceGeneratorFramework.Testing/MemberQueryExtensions.cs
@@ -13,7 +13,9 @@ public static class MemberQueryExtensions
// Properties
// ---------------------------------------------------------------------------------------------
- /// Gets a property declared on the type, optionally matching its type.
+ ///
+ /// Gets a property declared on the type, optionally matching its type.
+ ///
public static PropertyDeclarationSyntax GetProperty(
this TypeDeclarationSyntax type,
CodeQuery query,
@@ -26,7 +28,9 @@ public static PropertyDeclarationSyntax GetProperty(
$"No property named '{name}' was found on '{type.Identifier.ValueText}'."
);
- /// Determines whether the type declares a property with the given name, optionally matching its type.
+ ///
+ /// Determines whether the type declares a property with the given name, optionally matching its type.
+ ///
public static bool HasProperty(
this TypeDeclarationSyntax type,
CodeQuery query,
@@ -34,7 +38,9 @@ public static bool HasProperty(
TypeReference? propertyType = null
) => type.TryGetProperty(query, name, out _, propertyType);
- /// Attempts to get a property declared on the type, optionally matching its type.
+ ///
+ /// Attempts to get a property declared on the type, optionally matching its type.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
public static bool TryGetProperty(
this TypeDeclarationSyntax type,
@@ -71,7 +77,9 @@ public static bool TryGetProperty(
// Indexers
// ---------------------------------------------------------------------------------------------
- /// Gets an indexer declared on the type, optionally matching its type and index parameters.
+ ///
+ /// Gets an indexer declared on the type, optionally matching its type and index parameters.
+ ///
public static IndexerDeclarationSyntax GetIndexer(
this TypeDeclarationSyntax type,
CodeQuery query,
@@ -82,7 +90,9 @@ params TypeReference[]? indexParameters
? indexer!
: throw new SyntaxNotFoundException($"No matching indexer was found on '{type.Identifier.ValueText}'.");
- /// Determines whether the type declares an indexer, optionally matching its type and index parameters.
+ ///
+ /// Determines whether the type declares an indexer, optionally matching its type and index parameters.
+ ///
public static bool HasIndexer(
this TypeDeclarationSyntax type,
CodeQuery query,
@@ -90,7 +100,9 @@ public static bool HasIndexer(
params TypeReference[]? indexParameters
) => type.TryGetIndexer(query, out _, indexerType, indexParameters);
- /// Attempts to get an indexer declared on the type, optionally matching its type and index parameters.
+ ///
+ /// Attempts to get an indexer declared on the type, optionally matching its type and index parameters.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
public static bool TryGetIndexer(
this TypeDeclarationSyntax type,
@@ -126,7 +138,9 @@ params TypeReference[]? indexParameters
// Methods
// ---------------------------------------------------------------------------------------------
- /// Gets a method declared on the type, optionally matching its parameter types.
+ ///
+ /// Gets a method declared on the type, optionally matching its parameter types.
+ ///
public static MethodDeclarationSyntax GetMethod(
this TypeDeclarationSyntax type,
CodeQuery query,
@@ -139,7 +153,9 @@ params TypeReference[]? parameters
$"No method named '{name}' was found on '{type.Identifier.ValueText}'."
);
- /// Determines whether the type declares a method with the given name, optionally matching its parameter types.
+ ///
+ /// Determines whether the type declares a method with the given name, optionally matching its parameter types.
+ ///
public static bool HasMethod(
this TypeDeclarationSyntax type,
CodeQuery query,
@@ -147,7 +163,9 @@ public static bool HasMethod(
params TypeReference[]? parameters
) => type.TryGetMethod(query, name, out _, parameters);
- /// Attempts to get a method declared on the type, optionally matching its parameter types.
+ ///
+ /// Attempts to get a method declared on the type, optionally matching its parameter types.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
public static bool TryGetMethod(
this TypeDeclarationSyntax type,
@@ -216,7 +234,9 @@ TypeReference returnType
// Constructors
// ---------------------------------------------------------------------------------------------
- /// Gets a constructor declared on the type, optionally matching its parameter types.
+ ///
+ /// Gets a constructor declared on the type, optionally matching its parameter types.
+ ///
public static ConstructorDeclarationSyntax GetConstructor(
this TypeDeclarationSyntax type,
CodeQuery query,
@@ -226,14 +246,18 @@ params TypeReference[]? parameters
? constructor!
: throw new SyntaxNotFoundException($"No constructor was found on '{type.Identifier.ValueText}'.");
- /// Determines whether the type declares a constructor, optionally matching its parameter types.
+ ///
+ /// Determines whether the type declares a constructor, optionally matching its parameter types.
+ ///
public static bool HasConstructor(
this TypeDeclarationSyntax type,
CodeQuery query,
params TypeReference[]? parameters
) => type.TryGetConstructor(query, out _, parameters);
- /// Attempts to get a constructor declared on the type, optionally matching its parameter types.
+ ///
+ /// Attempts to get a constructor declared on the type, optionally matching its parameter types.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")]
public static bool TryGetConstructor(
this TypeDeclarationSyntax type,
diff --git a/src/src/SourceGeneratorFramework.Testing/RefactorTestOptions.cs b/src/src/SourceGeneratorFramework.Testing/RefactorTestOptions.cs
index c89ebd9..7e8f56a 100644
--- a/src/src/SourceGeneratorFramework.Testing/RefactorTestOptions.cs
+++ b/src/src/SourceGeneratorFramework.Testing/RefactorTestOptions.cs
@@ -3,24 +3,38 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// Options that configure a refactoring test run.
+///
+/// Options that configure a refactoring test run.
+///
public record RefactorTestOptions : SourceGeneratorTestOptions
{
- /// Gets the index of the registered code action to apply.
+ ///
+ /// Gets the index of the registered code action to apply.
+ ///
public int CodeActionIndex { get; init; }
- /// Gets the equivalence key used to select a registered code action.
- /// When specified, this takes precedence over .
+ ///
+ /// Gets the equivalence key used to select a registered code action.
+ ///
+ ///
+ /// When specified, this takes precedence over .
+ ///
public string? EquivalenceKey { get; init; }
- /// Gets the span the refactoring is triggered on.
- /// Either or must be provided.
+ ///
+ /// Gets the span the refactoring is triggered on.
+ ///
+ ///
+ /// Either or must be provided.
+ ///
public TextSpan? Span { get; init; }
///
/// Gets a selector that locates the node the refactoring is triggered on, using the input compilation's
/// . For example, query => query.GetMethod("M").
///
- /// Either or must be provided.
+ ///
+ /// Either or must be provided.
+ ///
public Func? NodeSelector { get; init; }
}
diff --git a/src/src/SourceGeneratorFramework.Testing/RefactorTestResult.cs b/src/src/SourceGeneratorFramework.Testing/RefactorTestResult.cs
index c0b7255..d2ce4d8 100644
--- a/src/src/SourceGeneratorFramework.Testing/RefactorTestResult.cs
+++ b/src/src/SourceGeneratorFramework.Testing/RefactorTestResult.cs
@@ -4,7 +4,9 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// The result of a refactoring test run.
+///
+/// The result of a refactoring test run.
+///
/// The code actions registered by the refactoring provider.
/// The refactored source of each document, keyed by document name.
/// The solution after applying the selected refactoring.
diff --git a/src/src/SourceGeneratorFramework.Testing/RefactoringTestRunner.cs b/src/src/SourceGeneratorFramework.Testing/RefactoringTestRunner.cs
index 485cb4f..5118d54 100644
--- a/src/src/SourceGeneratorFramework.Testing/RefactoringTestRunner.cs
+++ b/src/src/SourceGeneratorFramework.Testing/RefactoringTestRunner.cs
@@ -6,18 +6,24 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// Executes a code refactoring against a test document and returns the refactored source.
+///
+/// Executes a code refactoring against a test document and returns the refactored source.
+///
public sealed class RefactoringTestRunner : RoslynTestRunner
where TRefactoring : CodeRefactoringProvider, new()
{
- /// Runs the refactoring against one source file.
+ ///
+ /// Runs the refactoring against one source file.
+ ///
public Task RunAsync(
string source,
RefactorTestOptions? options = null,
CancellationToken cancellationToken = default
) => RunAsync([source], options, cancellationToken);
- /// Runs the refactoring against the supplied source files.
+ ///
+ /// Runs the refactoring against the supplied source files.
+ ///
public async Task RunAsync(
IEnumerable sources,
RefactorTestOptions? options = null,
diff --git a/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs b/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs
index 59d890c..e638164 100644
--- a/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs
+++ b/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs
@@ -7,10 +7,14 @@
namespace Purview.SourceGeneratorFramework.Testing;
-/// Provides shared Roslyn project creation for analyzer and code fix test runners.
+///
+/// Provides shared Roslyn project creation for analyzer and code fix test runners.
+///
public abstract class RoslynTestRunner
{
- /// Creates a compilation-with-analyzers using the configured analyzer options.
+ ///
+ /// Creates a compilation-with-analyzers using the configured analyzer options.
+ ///
protected static CompilationWithAnalyzers WithAnalyzers(
Compilation compilation,
ImmutableArray analyzers,
@@ -36,7 +40,9 @@ SourceGeneratorTestOptions options
: compilation.WithAnalyzers(analyzers, options.AnalyzerOptions);
}
- /// Creates a project containing the supplied sources.
+ ///
+ /// Creates a project containing the supplied sources.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Maintainability",
"CA1506:Avoid excessive class coupling",
@@ -99,17 +105,23 @@ Assembly componentAssembly
static string PrepareSource(string source, SourceGeneratorTestOptions options) =>
SourceGeneratorHelpers.PrepareSource(source, options);
- /// Owns the workspace and project created for a test run.
+ ///
+ /// Owns the workspace and project created for a test run.
+ ///
protected sealed class TestProject(
AdhocWorkspace workspace,
Project project,
ImmutableArray documentIds
) : IDisposable
{
- /// Gets the test project.
+ ///
+ /// Gets the test project.
+ ///
public Project Project { get; } = project;
- /// Gets the source document identifiers in input order.
+ ///
+ /// Gets the source document identifiers in input order.
+ ///
public ImmutableArray DocumentIds { get; } = documentIds;
///
diff --git a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs
index 70f5fdc..8375db6 100644
--- a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs
+++ b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs
@@ -40,7 +40,9 @@ public static SourceGeneratorTestOptions Default
///
/// Initializes a new instance by copying the current options.
///
- /// Derived records implicitly call this constructor unless they select another base constructor.
+ ///
+ /// Derived records implicitly call this constructor unless they select another base constructor.
+ ///
public SourceGeneratorTestOptions()
: this(Default) { }
@@ -143,7 +145,9 @@ public SourceGeneratorTestOptions()
///
public bool ValidateCodeWriterScopes { get; init; } = true;
- /// Gets whether framework source-generator logging is captured for this test run.
+ ///
+ /// Gets whether framework source-generator logging is captured for this test run.
+ ///
public bool EnableLogging { get; init; } = true;
///
@@ -210,7 +214,9 @@ public SourceGeneratorTestOptions()
///
/// Gets additional source text to include in the test compilation, such as generated code or other content that can be read by the generator.
///
- /// This will be added to the compilation as additional source files, along with the source provided.
+ ///
+ /// This will be added to the compilation as additional source files, along with the source provided.
+ ///
public ImmutableArray AdditionalSources { get; init; } = [];
///
@@ -224,12 +230,16 @@ public SourceGeneratorTestOptions()
///
/// Gets the options to use when running the compilation with analyzers.
///
- /// This is mutually exclusive with .
+ ///
+ /// This is mutually exclusive with .
+ ///
public AnalyzerOptions? AnalyzerOptions { get; init; }
///
/// Gets the options to use when running the compilation with analyzers.
///
- /// This is mutually exclusive with .
+ ///
+ /// This is mutually exclusive with .
+ ///
public CompilationWithAnalyzersOptions? CompilationWithAnalyzersOptions { get; init; }
}
diff --git a/src/src/SourceGeneratorFramework.Testing/SyntaxNotFoundException.cs b/src/src/SourceGeneratorFramework.Testing/SyntaxNotFoundException.cs
index 2b494b9..10ef262 100644
--- a/src/src/SourceGeneratorFramework.Testing/SyntaxNotFoundException.cs
+++ b/src/src/SourceGeneratorFramework.Testing/SyntaxNotFoundException.cs
@@ -6,11 +6,15 @@ namespace Purview.SourceGeneratorFramework.Testing;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1032:Implement standard exception constructors")]
public sealed class SyntaxNotFoundException : InvalidOperationException
{
- /// Initializes a new instance of the class.
+ ///
+ /// Initializes a new instance of the class.
+ ///
public SyntaxNotFoundException(string message)
: base(message) { }
- /// Initializes a new instance of the class.
+ ///
+ /// Initializes a new instance of the class.
+ ///
public SyntaxNotFoundException(string message, Exception innerException)
: base(message, innerException) { }
}
diff --git a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props
index 3ad2cb5..388cc0d 100644
--- a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props
+++ b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props
@@ -18,6 +18,9 @@
Identifies the registered sink for an isolated source-generator logging session.
+
+ Carries the consuming project's LangVersion so generators can gate emitted features.
+
diff --git a/src/src/SourceGeneratorShared/AttributeArgumentOptions.cs b/src/src/SourceGeneratorShared/AttributeArgumentOptions.cs
index 7093352..cca363b 100644
--- a/src/src/SourceGeneratorShared/AttributeArgumentOptions.cs
+++ b/src/src/SourceGeneratorShared/AttributeArgumentOptions.cs
@@ -1,22 +1,34 @@
namespace Purview.SourceGeneratorFramework;
-/// Describes one positional or named attribute argument.
+///
+/// Describes one positional or named attribute argument.
+///
public readonly record struct AttributeArgumentOptions
{
- /// Creates a positional attribute argument.
+ ///
+ /// Creates a positional attribute argument.
+ ///
public AttributeArgumentOptions(string value, string? name = null, bool isPropertyAssignment = false) =>
(Value, Name, IsPropertyAssignment) = (value, name, isPropertyAssignment);
- /// Creates a positional Boolean attribute argument using a valid C# literal.
+ ///
+ /// Creates a positional Boolean attribute argument using a valid C# literal.
+ ///
public AttributeArgumentOptions(bool value, string? name = null, bool isPropertyAssignment = false) =>
(Value, Name, IsPropertyAssignment) = (value ? "true" : "false", name, isPropertyAssignment);
- /// Gets the argument expression.
+ ///
+ /// Gets the argument expression.
+ ///
public string Value { get; }
- /// Gets an optional constructor parameter or property name.
+ ///
+ /// Gets an optional constructor parameter or property name.
+ ///
public string? Name { get; init; }
- /// Gets whether a named argument uses property assignment (=) instead of constructor naming (:).
+ ///
+ /// Gets whether a named argument uses property assignment (=) instead of constructor naming (:).
+ ///
public bool IsPropertyAssignment { get; init; }
}
diff --git a/src/src/SourceGeneratorShared/AttributeDeclarationOptions.cs b/src/src/SourceGeneratorShared/AttributeDeclarationOptions.cs
index db6cdf5..ae40663 100644
--- a/src/src/SourceGeneratorShared/AttributeDeclarationOptions.cs
+++ b/src/src/SourceGeneratorShared/AttributeDeclarationOptions.cs
@@ -2,22 +2,34 @@
namespace Purview.SourceGeneratorFramework;
-/// Describes an attribute applied to a generated declaration.
+///
+/// Describes an attribute applied to a generated declaration.
+///
public readonly record struct AttributeDeclarationOptions
{
- /// Creates an attribute declaration from a structured type reference.
+ ///
+ /// Creates an attribute declaration from a structured type reference.
+ ///
public AttributeDeclarationOptions(TypeReference reference) => Reference = reference;
- /// Creates an attribute declaration from a structured type value.
+ ///
+ /// Creates an attribute declaration from a structured type value.
+ ///
public AttributeDeclarationOptions(TypeIdentity type)
: this(type.AsTypeReference()) { }
- /// Gets the structured attribute type.
+ ///
+ /// Gets the structured attribute type.
+ ///
public TypeReference Reference { get; }
- /// Gets an optional target such as return, field, or property.
+ ///
+ /// Gets an optional target such as return, field, or property.
+ ///
public string? Target { get; init; }
- /// Gets structured attribute arguments.
+ ///
+ /// Gets structured attribute arguments.
+ ///
public ImmutableArray Arguments { get; init; }
}
diff --git a/src/src/SourceGeneratorShared/CodeWriter.cs b/src/src/SourceGeneratorShared/CodeWriter.cs
index 4633ae5..26e9252 100644
--- a/src/src/SourceGeneratorShared/CodeWriter.cs
+++ b/src/src/SourceGeneratorShared/CodeWriter.cs
@@ -16,10 +16,10 @@ namespace Purview.SourceGeneratorFramework;
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
public sealed partial class CodeWriter
{
- const char IndentCharacter = '\t';
const char NewLineCharacter = '\n';
const int DefaultCapacity = 4096;
- const int IndentDisplayWidth = 4;
+ const int DefaultExpressionCapacity = 128;
+ const int DefaultIndentationSize = 4;
const int DefaultMaximumLineLength = 100;
int _indentLevel;
@@ -32,6 +32,16 @@ public sealed partial class CodeWriter
readonly StringBuilder _builder;
readonly Dictionary? _openScopes;
+ readonly char _indentCharacter;
+ readonly int _indentationSize;
+ readonly int _maximumLineLength;
+
+ ///
+ /// Gets whether opened scopes are tracked for validation. When , expensive
+ /// opening-stack-trace capture is skipped because undisposed scopes will not be reported.
+ ///
+ bool TracksOpenScopes => _openScopes is not null;
+
///
/// Initializes a new writer with required generator identity.
///
@@ -69,10 +79,37 @@ public CodeWriter(
IsNullableContextEnabled = settings.IsNullableContextEnabled;
ThrowOnUnclosedScopes = throwOnUnclosedScopes;
+ _indentationSize = settings.IndentationSize > 0 ? settings.IndentationSize : DefaultIndentationSize;
+ _maximumLineLength = settings.MaximumLineLength > 0 ? settings.MaximumLineLength : DefaultMaximumLineLength;
+ _indentCharacter = settings.IndentationStyle == IndentationStyle.Spaces ? ' ' : '\t';
+
if (throwOnUnclosedScopes)
_openScopes = [];
}
+ ///
+ /// Initializes an independent scratch writer that inherits this writer's current configuration
+ /// without allocating a fresh .
+ ///
+ /// The writer whose configuration is inherited.
+ /// The initial buffer capacity.
+ CodeWriter(CodeWriter source, int initialCapacity)
+ {
+ _builder = new(initialCapacity);
+ GeneratorName = source.GeneratorName;
+ GeneratorVersion = source.GeneratorVersion;
+ NullableDirectiveMode = source.NullableDirectiveMode;
+ IsNullableContextEnabled = source.IsNullableContextEnabled;
+ ThrowOnUnclosedScopes = source.ThrowOnUnclosedScopes;
+ DefaultIncludeGeneratedAttributes = source.DefaultIncludeGeneratedAttributes;
+ _indentCharacter = source._indentCharacter;
+ _indentationSize = source._indentationSize;
+ _maximumLineLength = source._maximumLineLength;
+
+ if (ThrowOnUnclosedScopes)
+ _openScopes = [];
+ }
+
///
/// Gets the number of characters currently written.
///
@@ -95,10 +132,14 @@ public CodeWriter(
///
public bool ThrowOnUnclosedScopes { get; }
- /// Gets the source generator name used by generated headers and attributes.
+ ///
+ /// Gets the source generator name used by generated headers and attributes.
+ ///
public string GeneratorName { get; }
- /// Gets the source generator version used by generated headers and attributes.
+ ///
+ /// Gets the source generator version used by generated headers and attributes.
+ ///
public string GeneratorVersion { get; }
///
@@ -246,7 +287,7 @@ public CodeWriter Comment(params string[] comments)
public CodeWriter WriteIndent()
{
if (_indentLevel != 0)
- _builder.Append(IndentCharacter, _indentLevel);
+ AppendIndentation();
_atLineStart = false;
return this;
@@ -320,7 +361,9 @@ public CodeWriter Write(char value)
///
/// The value to quote.
/// The current writer.
- /// This method does not escape characters contained in .
+ ///
+ /// This method does not escape characters contained in .
+ ///
/// writer.Quote("value"); // "value"
public CodeWriter Quote(string? value = null)
{
@@ -349,7 +392,9 @@ public CodeWriter Quote(string? value = null)
/// // }
public BlockScope OpenBlockScope(string? header = null) => OpenDelimitedBlockScope(header, "{", "}");
- /// Writes a complete block and invokes a callback for its body.
+ ///
+ /// Writes a complete block and invokes a callback for its body.
+ ///
/// writer.OpenBlock("if (enabled)", body => body.WriteLine("Run();"));
public CodeWriter OpenBlock(string? header, Action bodyWriter)
{
@@ -389,7 +434,9 @@ public BlockScope OpenDelimitedBlockScope(string? header, string? openingToken,
return TrackOpenBlockScope(header, closingToken);
}
- /// Writes a complete explicitly delimited block and invokes a callback for its body.
+ ///
+ /// Writes a complete explicitly delimited block and invokes a callback for its body.
+ ///
/// writer.OpenDelimitedBlock("items", "(", ");", body => body.WriteLine("value"));
public CodeWriter OpenDelimitedBlock(
string? header,
@@ -438,7 +485,9 @@ public BlockScope OpenDelimitedBlockWithHeaderScope(
return TrackOpenBlockScope(header, closingToken);
}
- /// Writes a complete delimited block with a callback-completed header and body.
+ ///
+ /// Writes a complete delimited block with a callback-completed header and body.
+ ///
/// writer.OpenDelimitedBlockWithHeader("Call", w => w.Write("(value)"), "{", "}", body => body.WriteLine("Run();"));
public CodeWriter OpenDelimitedBlockWithHeader(
string? header,
@@ -490,7 +539,9 @@ Action body
return this;
}
- /// Writes a structured method declaration and returns its body scope.
+ ///
+ /// Writes a structured method declaration and returns its body scope.
+ ///
/// The method declaration.
///
/// The method body scope, or an empty scope when an abstract or expression-bodied method was
@@ -552,7 +603,8 @@ void WriteMethodHeader(MethodDeclarationOptions declaration)
declaration.IsAbstract,
declaration.IsVirtual,
declaration.IsOverride,
- declaration.IsSealed
+ declaration.IsSealed,
+ isReadOnly: declaration.IsReadOnly
);
WriteIf(declaration.IsAsync, "async ").WriteIf(declaration.IsUnsafe, "unsafe ");
@@ -569,7 +621,9 @@ void WriteMethodHeader(MethodDeclarationOptions declaration)
WriteMethodGenericConstraints(declaration.GenericTypes);
}
- /// Writes a structured partial method declaration.
+ ///
+ /// Writes a structured partial method declaration.
+ ///
/// writer.WritePartialMethod(new MethodDeclarationOptions("OnChanged"));
public CodeWriter WritePartialMethod(MethodDeclarationOptions declaration)
{
@@ -577,7 +631,9 @@ public CodeWriter WritePartialMethod(MethodDeclarationOptions declaration)
return this;
}
- /// Writes a structured partial method declaration.
+ ///
+ /// Writes a structured partial method declaration.
+ ///
/// writer.WriteMethodExpression(new MethodDeclarationOptions("Count", "int") { ExpressionBody = "items.Count" });
public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration)
{
@@ -593,7 +649,9 @@ public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration)
return WriteMethod(declaration, _ => { });
}
- /// Writes an expression-bodied method using a callback for the expression.
+ ///
+ /// Writes an expression-bodied method using a callback for the expression.
+ ///
/// writer.WriteMethodExpression(new MethodDeclarationOptions("Count", "int"), expression => expression.Write("items.Count"));
public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration, Action writeExpression)
{
@@ -613,7 +671,9 @@ public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration, Ac
return this;
}
- /// Writes a structured method and invokes a callback for its body.
+ ///
+ /// Writes a structured method and invokes a callback for its body.
+ ///
/// writer.WriteMethod(new MethodDeclarationOptions("Run"), body => body.WriteLine("return;"));
public CodeWriter WriteMethod(MethodDeclarationOptions declaration, Action writeBody)
{
@@ -647,7 +707,94 @@ public CodeWriter WriteMethod(MethodDeclarationOptions declaration, ActionWrites an auto-property or expression-bodied property.
+ ///
+ /// Writes a structured operator declaration and returns its body scope.
+ ///
+ /// The operator declaration.
+ ///
+ /// The operator body scope, or an empty scope when an expression-bodied operator was emitted.
+ ///
+ /// using (writer.WriteOperatorScope(new OperatorDeclarationOptions("==", TypeLibrary.System.Boolean, left, right))) writer.WriteLine("return left.Equals(right);");
+ public BlockScope WriteOperatorScope(OperatorDeclarationOptions declaration)
+ {
+ if (declaration.ReturnType.IsEmpty)
+ return default;
+
+ ValidateOperatorDeclaration(declaration);
+ BeginWrittenItem(WrittenItemKind.Method);
+
+ if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes)
+ WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false);
+
+ WriteAttributes(declaration.Attributes);
+
+ if (declaration.Accessibility is { } accessibility)
+ WriteAccessibility(accessibility).Write(' ');
+
+ WriteIf(declaration.IsStatic, "static ");
+ switch (declaration.Kind)
+ {
+ case OperatorDeclarationKind.ImplicitConversion:
+ case OperatorDeclarationKind.ExplicitConversion:
+ Write(declaration.Kind == OperatorDeclarationKind.ImplicitConversion ? "implicit " : "explicit ");
+ Write("operator ").WriteTypeReference(declaration.ReturnType);
+ WriteParametersWithHeuristic([declaration.Left]);
+ break;
+
+ case OperatorDeclarationKind.Unary:
+ WriteTypeReference(declaration.ReturnType).Write(" operator ").Write(declaration.OperatorToken);
+ WriteParametersWithHeuristic([declaration.Left]);
+ break;
+
+ case OperatorDeclarationKind.Binary:
+ WriteTypeReference(declaration.ReturnType).Write(" operator ").Write(declaration.OperatorToken);
+ WriteParametersWithHeuristic([declaration.Left, declaration.Right]);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(declaration));
+ }
+
+ if (declaration.ExpressionBody is not null)
+ {
+ Write(" => ");
+ WriteExpression(declaration.ExpressionBody, expressionWriter: null);
+ WriteLine(";");
+ CompleteWrittenItem(WrittenItemKind.Method, _indentLevel);
+ return default;
+ }
+
+ NewLine();
+ return OpenBlockScope(WrittenItemKind.Method);
+ }
+
+ ///
+ /// Writes a structured operator declaration and invokes a callback for its body.
+ ///
+ /// The operator declaration.
+ /// The action that writes the operator body.
+ /// The current writer.
+ /// The operator has an expression body.
+ /// writer.WriteOperator(new OperatorDeclarationOptions("==", TypeLibrary.System.Boolean, left, right), body => body.WriteLine("return left.Equals(right);"));
+ public CodeWriter WriteOperator(OperatorDeclarationOptions declaration, Action writeBody)
+ {
+ if (writeBody is null)
+ throw new ArgumentNullException(nameof(writeBody));
+ if (declaration.ExpressionBody is not null)
+ throw new ArgumentException(
+ "A callback body cannot be supplied for an expression-bodied operator.",
+ nameof(declaration)
+ );
+
+ using (WriteOperatorScope(declaration))
+ writeBody(this);
+
+ return this;
+ }
+
+ ///
+ /// Writes an auto-property or expression-bodied property.
+ ///
/// writer.WriteProperty(new PropertyDeclarationOptions("Name", "string"));
public CodeWriter WriteProperty(PropertyDeclarationOptions declaration)
{
@@ -669,10 +816,22 @@ public CodeWriter WriteProperty(PropertyDeclarationOptions declaration)
}
Write(" { ");
- if (declaration.HasGetter)
- WriteAccessor(declaration.GetterAccessibility, "get;");
- if (declaration.HasSetter || declaration.IsInitOnly)
- WriteAccessor(declaration.SetterAccessibility, declaration.IsInitOnly ? "init;" : "set;");
+ if (declaration.IsFieldBacked)
+ {
+ // C# 14 field-keyword semi-auto property: accessors reference the implicit backing field.
+ if (declaration.HasGetter)
+ Write("get => field; ");
+ if (declaration.HasSetter || declaration.IsInitOnly)
+ Write(declaration.IsInitOnly ? "init => field = value; " : "set => field = value; ");
+ }
+ else
+ {
+ if (declaration.HasGetter)
+ WriteAccessor(declaration.GetterAccessibility, "get;");
+ if (declaration.HasSetter || declaration.IsInitOnly)
+ WriteAccessor(declaration.SetterAccessibility, declaration.IsInitOnly ? "init;" : "set;");
+ }
+
Write("}");
if (declaration.Initializer is not null)
{
@@ -685,7 +844,9 @@ public CodeWriter WriteProperty(PropertyDeclarationOptions declaration)
return this;
}
- /// Writes an expression-bodied property using a callback for the expression.
+ ///
+ /// Writes an expression-bodied property using a callback for the expression.
+ ///
/// writer.WritePropertyExpression(new PropertyDeclarationOptions("Count", "int"), expression => expression.Write("items.Count"));
public CodeWriter WritePropertyExpression(
PropertyDeclarationOptions declaration,
@@ -717,7 +878,9 @@ Action writeExpression
return this;
}
- /// Writes a property with callback-generated accessor bodies.
+ ///
+ /// Writes a property with callback-generated accessor bodies.
+ ///
/// writer.WriteProperty(new PropertyDeclarationOptions("Value", "int"), get => get.WriteLine("return _value;"), null);
public CodeWriter WriteProperty(
PropertyDeclarationOptions declaration,
@@ -726,9 +889,9 @@ public CodeWriter WriteProperty(
)
{
ValidatePropertyDeclaration(declaration);
- if (declaration.ExpressionBody is not null || declaration.Initializer is not null)
+ if (declaration.ExpressionBody is not null || declaration.Initializer is not null || declaration.IsFieldBacked)
throw new ArgumentException(
- "A property with accessor bodies cannot specify an expression body or initializer.",
+ "A property with accessor bodies cannot specify an expression body, initializer, or the field keyword.",
nameof(declaration)
);
if (declaration.IsAbstract)
@@ -757,7 +920,111 @@ public CodeWriter WriteProperty(
return this;
}
- /// Writes a field declaration.
+ ///
+ /// Writes an indexer declaration with auto accessors or an expression body.
+ ///
+ /// The indexer declaration.
+ /// The current writer.
+ /// writer.WriteIndexer(new IndexerDeclarationOptions(Type("string"), new("index", Type("int"))));
+ public CodeWriter WriteIndexer(IndexerDeclarationOptions declaration)
+ {
+ if (declaration.Type.IsEmpty)
+ return this;
+ ValidateIndexerDeclaration(declaration);
+ BeginWrittenItem(WrittenItemKind.Property);
+ if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes)
+ WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false);
+ WriteAttributes(declaration.Attributes);
+ WriteIndexerHeader(declaration);
+ if (declaration.ExpressionBody is not null)
+ {
+ Write(" => ");
+ WriteExpression(declaration.ExpressionBody, expressionWriter: null);
+ WriteLine(";");
+ CompleteWrittenItem(WrittenItemKind.Property, _indentLevel);
+ return this;
+ }
+
+ Write(" { ");
+ if (declaration.HasGetter)
+ WriteAccessor(declaration.GetterAccessibility, "get;");
+ if (declaration.HasSetter || declaration.IsInitOnly)
+ WriteAccessor(declaration.SetterAccessibility, declaration.IsInitOnly ? "init;" : "set;");
+ Write("}");
+ NewLine();
+ CompleteWrittenItem(WrittenItemKind.Property, _indentLevel);
+ return this;
+ }
+
+ ///
+ /// Writes an indexer with callback-generated accessor bodies.
+ ///
+ /// The indexer declaration.
+ /// The action that writes the getter body, or for an auto getter.
+ /// The action that writes the setter body, or for an auto setter.
+ /// The current writer.
+ /// writer.WriteIndexer(new IndexerDeclarationOptions(Type("string"), new("index", Type("int"))), get => get.WriteLine("return _items[index];"), null);
+ public CodeWriter WriteIndexer(
+ IndexerDeclarationOptions declaration,
+ Action? writeGetterBody,
+ Action? writeSetterBody
+ )
+ {
+ ValidateIndexerDeclaration(declaration);
+ if (declaration.ExpressionBody is not null)
+ throw new ArgumentException(
+ "An indexer with accessor bodies cannot specify an expression body.",
+ nameof(declaration)
+ );
+ if (declaration.IsAbstract)
+ throw new ArgumentException(
+ "Accessor bodies cannot be supplied for an abstract indexer.",
+ nameof(declaration)
+ );
+
+ BeginWrittenItem(WrittenItemKind.Property);
+ if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes)
+ WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false);
+ WriteAttributes(declaration.Attributes);
+ WriteIndexerHeader(declaration).NewLine();
+ using (OpenBlockScope())
+ {
+ if (declaration.HasGetter)
+ WriteAccessorBody(declaration.GetterAccessibility, "get", writeGetterBody);
+ if (declaration.HasSetter || declaration.IsInitOnly)
+ WriteAccessorBody(
+ declaration.SetterAccessibility,
+ declaration.IsInitOnly ? "init" : "set",
+ writeSetterBody
+ );
+ }
+ CompleteWrittenItem(WrittenItemKind.Property, _indentLevel);
+ return this;
+ }
+
+ CodeWriter WriteIndexerHeader(IndexerDeclarationOptions declaration)
+ {
+ WriteMemberModifiers(
+ declaration.Accessibility,
+ declaration.IsStatic,
+ declaration.IsAbstract,
+ declaration.IsVirtual,
+ declaration.IsOverride,
+ declaration.IsSealed
+ );
+ WriteTypeReference(declaration.Type).Write(" this[");
+ for (var index = 0; index < declaration.Parameters.Length; index++)
+ {
+ if (index != 0)
+ Write(", ");
+ WriteParameter(declaration.Parameters[index]);
+ }
+ return Write(']');
+ }
+
+ ///
+ /// Writes a field declaration.
+ ///
/// writer.WriteField(new FieldDeclarationOptions("_value", "int"));
public CodeWriter WriteField(FieldDeclarationOptions declaration)
{
@@ -770,10 +1037,12 @@ public CodeWriter WriteField(FieldDeclarationOptions declaration)
WriteAttributes(declaration.Attributes);
if (declaration.Accessibility is { } accessibility)
WriteAccessibility(accessibility).Write(' ');
- WriteIf(declaration.IsConst, "const ")
+ WriteIf(declaration.IsRequired, "required ")
+ .WriteIf(declaration.IsConst, "const ")
.WriteIf(declaration.IsStatic && !declaration.IsConst, "static ")
.WriteIf(declaration.IsReadOnly, "readonly ")
.WriteIf(declaration.IsVolatile, "volatile ")
+ .WriteIf(declaration.IsRefField, "ref ")
.WriteTypeReference(declaration.Type)
.Write(' ')
.Write(declaration.Name);
@@ -791,13 +1060,66 @@ public CodeWriter WriteField(FieldDeclarationOptions declaration)
/// Writes a C# using directive.
///
/// The namespace to import.
+ /// Whether the directive is emitted as a global using.
/// The current writer.
/// writer.WriteUsing("System"); // using System;
- public CodeWriter WriteUsing(string namespaceName)
+ public CodeWriter WriteUsing(string namespaceName, bool isGlobal = false)
{
return string.IsNullOrWhiteSpace(namespaceName)
? throw new ArgumentException("Namespace cannot be null or whitespace.", nameof(namespaceName))
- : Write("using ").Write(namespaceName).WriteLine(";");
+ : Write(isGlobal ? "global using " : "using ").Write(namespaceName).WriteLine(";");
+ }
+
+ ///
+ /// Writes a C# using alias directive.
+ ///
+ /// The alias name.
+ /// The aliased namespace or type.
+ /// The current writer.
+ /// writer.WriteUsingAlias("Events", "global::Purview.Events"); // using Events = global::Purview.Events;
+ public CodeWriter WriteUsingAlias(string alias, string target)
+ {
+ if (string.IsNullOrWhiteSpace(alias))
+ throw new ArgumentException("Alias cannot be null or whitespace.", nameof(alias));
+ if (string.IsNullOrWhiteSpace(target))
+ throw new ArgumentException("Alias target cannot be null or whitespace.", nameof(target));
+
+ // The alias directive is not indented, so we don't call WriteIndentIfRequired().
+ return Write("using ").Write(alias).Write(" = ").Write(target).WriteLine(";");
+ }
+
+ ///
+ /// Writes a #region directive and returns a scope that restores indentation and emits
+ /// #endregion when disposed.
+ ///
+ /// The region name.
+ /// The region scope.
+ /// using (writer.OpenRegionScope("Generated members")) writer.WriteLine("public int Value { get; }");
+ public BlockScope OpenRegionScope(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Region name cannot be null or whitespace.", nameof(name));
+
+ EnsureBlankLine();
+ Write("#region ").WriteLine(name);
+ Indent();
+ return TrackOpenBlockScope(header: null, closingSeparator: "#endregion");
+ }
+
+ ///
+ /// Writes a #region and invokes a callback for its body.
+ ///
+ /// The region name.
+ /// The action that writes the region body.
+ /// The current writer.
+ /// writer.OpenRegion("Generated members", body => body.WriteProperty(new PropertyDeclarationOptions("Value", "int")));
+ public CodeWriter OpenRegion(string name, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (OpenRegionScope(name))
+ body(this);
+ return this;
}
///
@@ -843,7 +1165,9 @@ public CodeWriter WriteBlockNamespace(TypeReference typeReference, Action
typeReference is null ? NoOpScope.Instance : WriteBlockNamespaceScope(typeReference.Identity.Namespace);
- /// Writes a block-scoped namespace and invokes a callback for its body.
+ ///
+ /// Writes a block-scoped namespace and invokes a callback for its body.
+ ///
/// The namespace, or to omit the wrapper.
/// The action that writes the namespace body.
/// The current writer.
@@ -1069,14 +1393,18 @@ public CodeWriter WriteRecordStruct(TypeDeclarationOptions declaration, ActionWrites an interface declaration and returns its body scope.
+ ///
+ /// Writes an interface declaration and returns its body scope.
+ ///
/// using (writer.WriteInterfaceScope(new TypeDeclarationOptions("IService"))) { }
public BlockScope WriteInterfaceScope(TypeDeclarationOptions declaration) =>
declaration is null
? throw new ArgumentNullException(nameof(declaration))
: WriteTypeScope(declaration with { Kind = TypeDeclarationKind.Interface });
- /// Writes an interface declaration and invokes a callback for its body.
+ ///
+ /// Writes an interface declaration and invokes a callback for its body.
+ ///
/// writer.WriteInterface(new TypeDeclarationOptions("IService"), _ => { });
public CodeWriter WriteInterface(TypeDeclarationOptions declaration, Action bodyWriter)
{
@@ -1087,14 +1415,18 @@ public CodeWriter WriteInterface(TypeDeclarationOptions declaration, ActionWrites an enum declaration and returns its body scope.
+ ///
+ /// Writes an enum declaration and returns its body scope.
+ ///
/// using (writer.WriteEnumScope(new TypeDeclarationOptions("Status"))) { }
public BlockScope WriteEnumScope(TypeDeclarationOptions declaration) =>
declaration is null
? throw new ArgumentNullException(nameof(declaration))
: WriteTypeScope(declaration with { Kind = TypeDeclarationKind.Enum });
- /// Writes an enum declaration and invokes a callback for its body.
+ ///
+ /// Writes an enum declaration and invokes a callback for its body.
+ ///
/// writer.WriteEnum(new TypeDeclarationOptions("Status"), _ => { });
public CodeWriter WriteEnum(TypeDeclarationOptions declaration, Action bodyWriter)
{
@@ -1107,7 +1439,9 @@ public CodeWriter WriteEnum(TypeDeclarationOptions declaration, ActionWrites an enum declaration with structured field declarations.
+ ///
+ /// Writes an enum declaration with structured field declarations.
+ ///
/// The enum declaration options.
/// The fields to write in declaration order.
/// The current writer.
@@ -1131,7 +1465,9 @@ public CodeWriter WriteEnum(TypeDeclarationOptions declaration, params EnumField
);
}
- /// Writes a field in an enum declaration.
+ ///
+ /// Writes a field in an enum declaration.
+ ///
/// The enum field declaration options.
/// The current writer.
/// writer.WriteEnumField(new EnumFieldDeclarationOptions("Ready", 1));
@@ -1167,7 +1503,9 @@ void WriteXmlSummary(ImmutableArray summary)
WriteLine("/// ");
}
- /// Writes a complete delegate declaration.
+ ///
+ /// Writes a complete delegate declaration.
+ ///
/// writer.WriteDelegate(new TypeDeclarationOptions("Handler") { DelegateReturnType = "void" });
public CodeWriter WriteDelegate(TypeDeclarationOptions declaration)
{
@@ -1220,6 +1558,9 @@ or TypeDeclarationKind.RecordClass
else if (isClass && declaration.IsSealed)
Write("sealed ");
+ if (isStruct && declaration.IsRefStruct)
+ Write("ref ");
+
if (
declaration.IsPartial
&& declaration.Kind is not TypeDeclarationKind.Enum and not TypeDeclarationKind.Delegate
@@ -1272,7 +1613,9 @@ or TypeDeclarationKind.RecordClass
return OpenBlockScope(WrittenItemKind.Type);
}
- /// Writes a structured type declaration and invokes a callback for its body.
+ ///
+ /// Writes a structured type declaration and invokes a callback for its body.
+ ///
/// The structured type declaration options.
/// The action that writes the type body.
/// The current writer.
@@ -1325,7 +1668,9 @@ public BlockScope WriteConstructorScope(ConstructorDeclarationOptions declaratio
return OpenBlockScope(WrittenItemKind.Constructor);
}
- /// Writes a structured constructor and invokes a callback for its body.
+ ///
+ /// Writes a structured constructor and invokes a callback for its body.
+ ///
/// writer.WriteConstructor(new ConstructorDeclarationOptions("C"), _ => { });
public CodeWriter WriteConstructor(ConstructorDeclarationOptions declaration, Action writeBody)
{
@@ -1429,7 +1774,9 @@ public CodeWriter WriteGeneratedCodeAttribute(string generatorName, string? vers
.WriteLine("\")]");
}
- /// Writes the standard marker attributes for a generated declaration.
+ ///
+ /// Writes the standard marker attributes for a generated declaration.
+ ///
///
/// Whether to emit .
/// This must be enabled only for declaration targets supported by that attribute.
@@ -1522,7 +1869,9 @@ public CodeWriter MultiLineParameters(params string[] parameters)
return Unindent();
}
- /// Writes a method invocation statement.
+ ///
+ /// Writes a method invocation statement.
+ ///
/// The method name, optionally including a receiver.
/// The argument expressions.
/// The current writer.
@@ -1530,7 +1879,9 @@ public CodeWriter MultiLineParameters(params string[] parameters)
public CodeWriter WriteMethodCall(string methodName, params string[] arguments) =>
WriteMethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, false);
- /// Writes an awaited method invocation statement.
+ ///
+ /// Writes an awaited method invocation statement.
+ ///
/// The method name, optionally including a receiver.
/// The argument expressions.
/// The current writer.
@@ -1570,7 +1921,9 @@ public CodeWriter WriteMethodCall(
writeArgumentsOnSeparateLines
);
- /// Writes an awaited method invocation from structured argument declarations.
+ ///
+ /// Writes an awaited method invocation from structured argument declarations.
+ ///
/// The method name without a receiver or generic argument list.
/// The structured arguments to invoke the method with.
/// An optional receiver such as service.
@@ -1658,7 +2011,7 @@ bool isAwaited
return WriteLine(";");
}
- bool WriteMethodCallArguments(string?[] arguments, bool writeOnSeparateLines, string multilineClosingToken = ");")
+ bool WriteMethodCallArguments(string?[] arguments, bool writeOnSeparateLines, string multilineClosingSuffix = ";")
{
var inlineLength = CurrentLineLength + 2;
for (var index = 0; index < arguments.Length; index++)
@@ -1666,7 +2019,7 @@ bool isAwaited
var canWriteInline =
!writeOnSeparateLines
- && inlineLength <= DefaultMaximumLineLength
+ && inlineLength <= _maximumLineLength
&& arguments.All(static argument => argument is not null && !argument.Contains('\n'));
if (arguments.Length == 0)
{
@@ -1690,13 +2043,25 @@ bool isAwaited
for (var index = 0; index < arguments.Length; index++)
{
WriteExpression(arguments[index], expressionWriter: null);
- WriteLine(index == arguments.Length - 1 ? multilineClosingToken : ",");
+ if (index != arguments.Length - 1)
+ WriteLine(",");
+ else
+ NewLine();
}
Unindent();
+ Write(')');
+ if (multilineClosingSuffix.Length > 0)
+ {
+ Write(multilineClosingSuffix);
+ NewLine();
+ }
+
return true;
}
- /// Writes an assignment statement.
+ ///
+ /// Writes an assignment statement.
+ ///
/// The target, such as value or var result.
/// The assigned expression.
/// Whether to force the value to be not null, by appending the null-forgiving operator (!).
@@ -1713,7 +2078,9 @@ public CodeWriter WriteAssignment(string target, string value, bool forceNotNull
return WriteLine(";");
}
- /// Writes an assignment statement using a callback for a multiline expression.
+ ///
+ /// Writes an assignment statement using a callback for a multiline expression.
+ ///
/// writer.WriteAssignment("value", expression => expression.Write("new Value()"));
public CodeWriter WriteAssignment(string target, Action writeValue)
{
@@ -1725,7 +2092,9 @@ public CodeWriter WriteAssignment(string target, Action writeValue)
return WriteLine(";");
}
- /// Writes an assignment whose value is a structured object-creation expression.
+ ///
+ /// Writes an assignment whose value is a structured object-creation expression.
+ ///
/// writer.WriteAssignment("@event", new ObjectCreationOptions(eventType, "propVal1", "propVal2"));
public CodeWriter WriteAssignment(string target, ObjectCreationOptions value, bool forceNotNull = false)
{
@@ -1739,7 +2108,9 @@ public CodeWriter WriteAssignment(string target, ObjectCreationOptions value, bo
return WriteLine(";");
}
- /// Writes a typed local or declaration assignment.
+ ///
+ /// Writes a typed local or declaration assignment.
+ ///
/// writer.WriteAssignment("var", "value", "CreateValue()");
public CodeWriter WriteAssignment(string type, string name, string value, bool forceNotNull = false)
{
@@ -1748,7 +2119,9 @@ public CodeWriter WriteAssignment(string type, string name, string value, bool f
return WriteAssignment($"{type} {name}", value, forceNotNull);
}
- /// Writes a typed local or declaration assignment with a multiline expression.
+ ///
+ /// Writes a typed local or declaration assignment with a multiline expression.
+ ///
/// writer.WriteAssignment("Value", "value", expression => expression.Write("CreateValue()"));
public CodeWriter WriteAssignment(string type, string name, Action writeValue)
{
@@ -1757,7 +2130,9 @@ public CodeWriter WriteAssignment(string type, string name, Action w
return WriteAssignment($"{type} {name}", writeValue);
}
- /// Writes a typed local assignment whose value is a structured object creation.
+ ///
+ /// Writes a typed local assignment whose value is a structured object creation.
+ ///
/// writer.WriteAssignment("var", "@event", new ObjectCreationOptions(eventType, "propVal1", "propVal2"));
public CodeWriter WriteAssignment(string type, string name, ObjectCreationOptions value, bool forceNotNull = false)
{
@@ -1768,17 +2143,104 @@ public CodeWriter WriteAssignment(string type, string name, ObjectCreationOption
bool WriteObjectCreationExpression(ObjectCreationOptions value, bool forceNotNull)
{
- Write("new ").WriteTypeReference(value.Reference).Write('(');
+ ValidateInitializerMembers(value);
+ Write("new ").WriteTypeReference(value.Reference);
+
+ var hasInitializer = !value.InitializerMembers.IsDefaultOrEmpty;
string[] arguments = value.Arguments.IsDefault ? [] : [.. value.Arguments.Select(RenderCallArgument)];
- if (WriteMethodCallArguments(arguments, value.WriteArgumentsOnSeparateLines, forceNotNull ? ")!;" : ");"))
- return true;
- Write(')');
+ if (arguments.Length > 0 || !hasInitializer)
+ {
+ // WriteMethodCallArguments writes the closing parenthesis itself: inline or for empty
+ // arguments it emits ')' and returns false, while a multiline layout emits the closing
+ // token and returns true.
+ Write('(');
+ if (
+ WriteMethodCallArguments(
+ arguments,
+ value.WriteArgumentsOnSeparateLines,
+ hasInitializer ? string.Empty
+ : forceNotNull ? "!;"
+ : ";"
+ )
+ )
+ {
+ // Multiline arguments: the closing token was already written. When an initializer
+ // follows, the initializer supplies the terminating semicolon via the caller.
+ if (hasInitializer)
+ {
+ WriteObjectInitializer(value, forceNotNull);
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ if (hasInitializer)
+ {
+ WriteObjectInitializer(value, forceNotNull);
+ return false;
+ }
+
if (forceNotNull)
Write('!');
+
return false;
}
- /// Writes a return statement.
+ bool WriteObjectInitializer(ObjectCreationOptions value, bool forceNotNull)
+ {
+ if (value.WriteInitializerMembersOnSeparateLines)
+ {
+ EnsureNewLine();
+ WriteLine("{");
+ Indent();
+ for (var index = 0; index < value.InitializerMembers.Length; index++)
+ {
+ var member = value.InitializerMembers[index];
+ Write(member.Name).Write(" = ").Write(member.Value).WriteLine(",");
+ }
+
+ Unindent();
+ Write("}");
+ }
+ else
+ {
+ Write(" { ");
+ for (var index = 0; index < value.InitializerMembers.Length; index++)
+ {
+ if (index != 0)
+ Write(" ");
+
+ var member = value.InitializerMembers[index];
+ Write(member.Name).Write(" = ").Write(member.Value).Write(",");
+ }
+
+ Write(" }");
+ }
+
+ if (forceNotNull)
+ Write('!');
+
+ return false;
+ }
+
+ static void ValidateInitializerMembers(ObjectCreationOptions value)
+ {
+ for (
+ var index = 0;
+ !value.InitializerMembers.IsDefaultOrEmpty && index < value.InitializerMembers.Length;
+ index++
+ )
+ {
+ ValidateRequired(value.InitializerMembers[index].Name, "Initializer member name", nameof(value));
+ ValidateRequired(value.InitializerMembers[index].Value, "Initializer member value", nameof(value));
+ }
+ }
+
+ ///
+ /// Writes a return statement.
+ ///
/// writer.WriteReturn("value"); // return value;
public CodeWriter WriteReturn(string? expression = null)
{
@@ -1789,7 +2251,9 @@ public CodeWriter WriteReturn(string? expression = null)
return WriteLine(";");
}
- /// Writes a return statement using a callback for a multiline expression.
+ ///
+ /// Writes a return statement using a callback for a multiline expression.
+ ///
/// writer.WriteReturn(expression => expression.Write("value"));
public CodeWriter WriteReturn(Action writeExpression)
{
@@ -1800,7 +2264,9 @@ public CodeWriter WriteReturn(Action writeExpression)
return WriteLine(";");
}
- /// Writes a throw statement.
+ ///
+ /// Writes a throw statement.
+ ///
/// writer.WriteThrow("new InvalidOperationException()");
public CodeWriter WriteThrow(string expression)
{
@@ -1810,23 +2276,38 @@ public CodeWriter WriteThrow(string expression)
return WriteLine(";");
}
- /// Writes a throw statement.
- /// writer.WriteThrow("new InvalidOperationException()");
+ ///
+ /// Writes a throw statement using a structured exception type and an optional message.
+ ///
+ /// The exception type to throw.
+ ///
+ /// The exception message written as a string literal, or to throw the
+ /// exception without a message. Backslashes and double quotes are escaped so raw literal text
+ /// can be supplied.
+ ///
+ /// writer.WriteThrow(TypeLibrary.System.InvalidOperationException, "Cannot be null.");
public CodeWriter WriteThrow(TypeReference exceptionType, string? message = null)
{
if (exceptionType.IsNullOrEmpty())
throw new ArgumentException("Exception type cannot be null or empty.", nameof(exceptionType));
Write("throw new ");
- WriteExpression(
- $"{exceptionType}{(message is null ? string.Empty : $"(\"{message}\")")}",
- expressionWriter: null
- );
+ if (message is null)
+ WriteExpression($"{exceptionType}()", expressionWriter: null);
+ else
+ {
+ WriteExpression(
+ $"{exceptionType}(\"{message.Replace("\\", "\\\\").Replace("\"", "\\\"")}\")",
+ expressionWriter: null
+ );
+ }
return WriteLine(";");
}
- /// Writes a throw statement using a callback for a multiline expression.
+ ///
+ /// Writes a throw statement using a callback for a multiline expression.
+ ///
/// writer.WriteThrow(expression => expression.Write("new InvalidOperationException()"));
public CodeWriter WriteThrow(Action writeExpression)
{
@@ -1857,7 +2338,9 @@ public CodeWriter WriteIfBlock(string condition, Action bodyWriter)
return this;
}
- /// Writes an if statement and returns its body scope.
+ ///
+ /// Writes an if statement and returns its body scope.
+ ///
/// using (writer.WriteIfBlockScope("enabled")) writer.WriteReturn();
public BlockScope WriteIfBlockScope(string condition)
{
@@ -1868,6 +2351,332 @@ public BlockScope WriteIfBlockScope(string condition)
return OpenBlockScope();
}
+ ///
+ /// Writes an if statement and an optional else block.
+ ///
+ /// The if condition.
+ /// The action that writes the if body.
+ /// The action that writes the else body, or to omit the else.
+ /// The current writer.
+ /// writer.WriteIfElse("enabled", body => body.WriteReturn("value"), null);
+ public CodeWriter WriteIfElse(string condition, Action ifBody, Action? elseBody)
+ {
+ if (ifBody is null)
+ throw new ArgumentNullException(nameof(ifBody));
+ using (WriteIfBlockScope(condition))
+ ifBody(this);
+ if (elseBody is not null)
+ WriteElse(elseBody);
+ return this;
+ }
+
+ ///
+ /// Writes an else block following an if and invokes a callback for its body.
+ ///
+ /// The action that writes the else body.
+ /// The current writer.
+ /// writer.WriteIfBlock("enabled", body => body.WriteReturn("value")).WriteElse(body => body.WriteReturn("null"));
+ public CodeWriter WriteElse(Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteElseScope())
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes an else block and returns its body scope.
+ ///
+ /// The else body scope.
+ /// using (writer.WriteIfBlockScope("enabled")) writer.WriteReturn("value"); using (writer.WriteElseScope()) writer.WriteReturn("null");
+ public BlockScope WriteElseScope()
+ {
+ EnsureNewLine();
+ WriteLine("else");
+ return OpenBlockScope();
+ }
+
+ ///
+ /// Writes a foreach statement and invokes a callback for its body.
+ ///
+ /// The iterator declaration, such as var item in items.
+ /// The action that writes the loop body.
+ /// The current writer.
+ /// writer.WriteForeach("var item in items", body => body.WriteMethodCall("Process", "item"));
+ public CodeWriter WriteForeach(string iterator, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteForeachScope(iterator))
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a foreach statement and returns its body scope.
+ ///
+ /// The iterator declaration, such as var item in items.
+ /// The loop body scope.
+ /// using (writer.WriteForeachScope("var item in items")) writer.WriteMethodCall("Process", "item");
+ public BlockScope WriteForeachScope(string iterator)
+ {
+ ValidateStatementPart(iterator, nameof(iterator));
+ Write("foreach (").Write(iterator).WriteLine(")");
+ return OpenBlockScope();
+ }
+
+ ///
+ /// Writes a for statement and invokes a callback for its body.
+ ///
+ /// The initializer expression, or for none.
+ /// The condition expression, or for none.
+ /// The iterator expression, or for none.
+ /// The action that writes the loop body.
+ /// The current writer.
+ /// writer.WriteFor("int i = 0", "i < count", "i++", body => body.WriteMethodCall("Process", "items[i]"));
+ public CodeWriter WriteFor(string? initializer, string? condition, string? iterator, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteForScope(initializer, condition, iterator))
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a for statement and returns its body scope.
+ ///
+ /// The initializer expression, or for none.
+ /// The condition expression, or for none.
+ /// The iterator expression, or for none.
+ /// The loop body scope.
+ /// using (writer.WriteForScope("int i = 0", "i < count", "i++")) writer.WriteMethodCall("Process", "items[i]");
+ public BlockScope WriteForScope(string? initializer, string? condition, string? iterator)
+ {
+ Write("for (");
+ Write(initializer).Write("; ");
+ Write(condition).Write("; ");
+ Write(iterator).WriteLine(")");
+ return OpenBlockScope();
+ }
+
+ ///
+ /// Writes a while statement and invokes a callback for its body.
+ ///
+ /// The loop condition.
+ /// The action that writes the loop body.
+ /// The current writer.
+ /// writer.WriteWhile("queue.Count > 0", body => body.WriteMethodCall("Process", "queue.Dequeue()"));
+ public CodeWriter WriteWhile(string condition, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteWhileScope(condition))
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a while statement and returns its body scope.
+ ///
+ /// The loop condition.
+ /// The loop body scope.
+ /// using (writer.WriteWhileScope("queue.Count > 0")) writer.WriteMethodCall("Process", "queue.Dequeue()");
+ public BlockScope WriteWhileScope(string condition)
+ {
+ ValidateStatementPart(condition, nameof(condition));
+ Write("while (").Write(condition).WriteLine(")");
+ return OpenBlockScope();
+ }
+
+ ///
+ /// Writes a do-while statement and invokes a callback for its body.
+ ///
+ /// The trailing loop condition.
+ /// The action that writes the loop body.
+ /// The current writer.
+ /// writer.WriteDoWhile("!finished", body => body.WriteMethodCall("Advance"));
+ public CodeWriter WriteDoWhile(string condition, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteDoWhileScope(condition))
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a do-while statement and returns its body scope.
+ ///
+ /// The trailing loop condition.
+ /// The loop body scope, which writes } while (condition); when disposed.
+ /// using (writer.WriteDoWhileScope("!finished")) writer.WriteMethodCall("Advance");
+ public BlockScope WriteDoWhileScope(string condition)
+ {
+ ValidateStatementPart(condition, nameof(condition));
+ return OpenDelimitedBlockScope("do", "{", "} while (" + condition + ");");
+ }
+
+ ///
+ /// Writes a try block and invokes a callback for its body.
+ ///
+ /// The action that writes the try body.
+ /// The current writer.
+ /// writer.WriteTry(body => body.WriteMethodCall("Run"));
+ public CodeWriter WriteTry(Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteTryScope())
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a try block and returns its body scope.
+ ///
+ /// The try body scope.
+ /// using (writer.WriteTryScope()) writer.WriteMethodCall("Run");
+ public BlockScope WriteTryScope() => OpenDelimitedBlockScope("try", "{", "}");
+
+ ///
+ /// Writes a catch block and invokes a callback for its body.
+ ///
+ /// The action that writes the catch body.
+ /// The current writer.
+ /// writer.WriteCatch(body => body.WriteThrow(TypeLibrary.System.InvalidOperationException, "Failed"));
+ public CodeWriter WriteCatch(Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteCatchScope())
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a typed catch block and invokes a callback for its body.
+ ///
+ /// The caught exception type, or for a bare catch.
+ /// The exception variable name, or to omit it.
+ /// The action that writes the catch body.
+ /// The current writer.
+ /// writer.WriteCatch(TypeLibrary.System.Exception, "ex", body => body.WriteMethodCall("Log", "ex"));
+ public CodeWriter WriteCatch(TypeReference? exceptionType, string? name, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteCatchScope(exceptionType, name))
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a catch block and returns its body scope.
+ ///
+ /// The caught exception type, or for a bare catch.
+ /// The exception variable name, or to omit it.
+ /// The catch body scope.
+ /// using (writer.WriteCatchScope(TypeLibrary.System.Exception, "ex")) writer.WriteMethodCall("Log", "ex");
+ public BlockScope WriteCatchScope(TypeReference? exceptionType = null, string? name = null)
+ {
+ Write("catch");
+ if (exceptionType is not null)
+ {
+ Write(" (").WriteTypeReference(exceptionType);
+ if (!string.IsNullOrWhiteSpace(name))
+ Write(' ').Write(name);
+ Write(')');
+ }
+ WriteLine();
+ return OpenBlockScope();
+ }
+
+ ///
+ /// Writes a finally block and invokes a callback for its body.
+ ///
+ /// The action that writes the finally body.
+ /// The current writer.
+ /// writer.WriteFinally(body => body.WriteMethodCall("Dispose"));
+ public CodeWriter WriteFinally(Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteFinallyScope())
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a finally block and returns its body scope.
+ ///
+ /// The finally body scope.
+ /// using (writer.WriteFinallyScope()) writer.WriteMethodCall("Dispose");
+ public BlockScope WriteFinallyScope()
+ {
+ WriteLine("finally");
+ return OpenBlockScope();
+ }
+
+ ///
+ /// Writes a using statement and invokes a callback for its body.
+ ///
+ /// The resource declaration, such as var stream = Open().
+ /// The action that writes the using body.
+ /// The current writer.
+ /// writer.WriteUsingStatement("var stream = Open()", body => body.WriteMethodCall("Read", "stream"));
+ public CodeWriter WriteUsingStatement(string declaration, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteUsingStatementScope(declaration))
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a using statement and returns its body scope.
+ ///
+ /// The resource declaration, such as var stream = Open().
+ /// The using body scope.
+ /// using (writer.WriteUsingStatementScope("var stream = Open()")) writer.WriteMethodCall("Read", "stream");
+ public BlockScope WriteUsingStatementScope(string declaration)
+ {
+ ValidateStatementPart(declaration, nameof(declaration));
+ Write("using (").Write(declaration).WriteLine(")");
+ return OpenBlockScope();
+ }
+
+ ///
+ /// Writes a lock statement and invokes a callback for its body.
+ ///
+ /// The lock expression.
+ /// The action that writes the lock body.
+ /// The current writer.
+ /// writer.WriteLockStatement("_gate", body => body.WriteMethodCall("Run"));
+ public CodeWriter WriteLockStatement(string expression, Action body)
+ {
+ if (body is null)
+ throw new ArgumentNullException(nameof(body));
+ using (WriteLockStatementScope(expression))
+ body(this);
+ return this;
+ }
+
+ ///
+ /// Writes a lock statement and returns its body scope.
+ ///
+ /// The lock expression.
+ /// The lock body scope.
+ /// using (writer.WriteLockStatementScope("_gate")) writer.WriteMethodCall("Run");
+ public BlockScope WriteLockStatementScope(string expression)
+ {
+ ValidateStatementPart(expression, nameof(expression));
+ Write("lock (").Write(expression).WriteLine(")");
+ return OpenBlockScope();
+ }
+
///
/// Writes a comma-separated collection with one item per line.
///
@@ -1938,6 +2747,49 @@ public CodeWriter WriteDelimited(IEnumerable items, string delimiter =
return this;
}
+ ///
+ /// Writes a C# collection expression such as [a, b, ..c], optionally one element per line with
+ /// the closing bracket on its own unindented line.
+ ///
+ /// The element expressions; spread elements such as ..source are passed verbatim.
+ /// Whether to write one element per line.
+ /// The current writer.
+ /// writer.WriteCollectionExpression(["first", "second", "..rest"]); // [first, second, ..rest]
+ public CodeWriter WriteCollectionExpression(IEnumerable items, bool writeOnSeparateLines = false)
+ {
+ if (items is null)
+ throw new ArgumentNullException(nameof(items));
+
+ var elements = items.ToArray();
+ Write('[');
+ if (elements.Length == 0)
+ return Write(']');
+
+ if (!writeOnSeparateLines)
+ {
+ for (var index = 0; index < elements.Length; index++)
+ {
+ if (index != 0)
+ Write(", ");
+ Write(elements[index]);
+ }
+
+ return Write(']');
+ }
+
+ NewLine().Indent();
+ for (var index = 0; index < elements.Length; index++)
+ {
+ Write(elements[index]);
+ if (index != elements.Length - 1)
+ WriteLine(",");
+ else
+ NewLine();
+ }
+ Unindent();
+ return Write(']');
+ }
+
///
/// Increases indentation until the returned scope is disposed.
///
@@ -1946,11 +2798,22 @@ public CodeWriter WriteDelimited(IEnumerable items, string delimiter =
public IndentScope IndentedScope()
{
Indent();
- return new(this, OpenScope("indentation", header: null, new StackTrace(1, fNeedFileInfo: true).ToString()));
+ return new(
+ this,
+ OpenScope(
+ "indentation",
+ header: null,
+ TracksOpenScopes ? new StackTrace(1, fNeedFileInfo: true).ToString() : string.Empty
+ )
+ );
}
- /// Invokes a callback at one additional indentation level.
- /// Invokes a callback at one additional indentation level.
+ ///
+ /// Invokes a callback at one additional indentation level.
+ ///
+ ///
+ /// Invokes a callback at one additional indentation level.
+ ///
/// The action to invoke while indented.
/// The current writer.
/// writer.Indented(body => body.WriteLine("value"));
@@ -1975,8 +2838,12 @@ public IndentScope IndentedScope(string line)
return IndentedScope();
}
- /// Writes a line and invokes a callback at one additional indentation level.
- /// Writes a line and invokes a callback at one additional indentation level.
+ ///
+ /// Writes a line and invokes a callback at one additional indentation level.
+ ///
+ ///
+ /// Writes a line and invokes a callback at one additional indentation level.
+ ///
/// The line to write before indenting.
/// The action to invoke while indented.
/// The current writer.
@@ -2024,20 +2891,27 @@ void WriteIndentIfRequired()
return;
if (_indentLevel != 0)
- _builder.Append(IndentCharacter, _indentLevel);
+ AppendIndentation();
_atLineStart = false;
}
+ void AppendIndentation()
+ {
+ if (_indentCharacter == '\t')
+ _builder.Append('\t', _indentLevel);
+ else
+ _builder.Append(' ', _indentLevel * _indentationSize);
+ }
+
void WriteExpression(string? expression, Action? expressionWriter)
{
var callback = expressionWriter;
if (callback is not null)
{
- CodeWriter expressionWriterBuffer = new(new GenerationSettings(GeneratorName, GeneratorVersion))
- {
- DefaultIncludeGeneratedAttributes = DefaultIncludeGeneratedAttributes,
- };
+ // Expressions are typically short, so use a small buffer and copy this writer's current
+ // settings directly rather than allocating a fresh GenerationSettings.
+ CodeWriter expressionWriterBuffer = new(this, DefaultExpressionCapacity);
callback!(expressionWriterBuffer);
expression = expressionWriterBuffer.ToString().TrimEnd(NewLineCharacter);
}
@@ -2075,7 +2949,7 @@ void WriteParametersWithHeuristic(ImmutableArray pa
inlineLength += GetParameterLength(parameters[index]) + (index == 0 ? 0 : 2);
Write('(');
- if (inlineLength <= DefaultMaximumLineLength)
+ if (inlineLength <= _maximumLineLength)
{
for (var index = 0; index < parameters.Length; index++)
{
@@ -2090,11 +2964,14 @@ void WriteParametersWithHeuristic(ImmutableArray pa
NewLine().Indent();
for (var index = 0; index < parameters.Length; index++)
{
- WriteParameter(parameters[index]).Write(index == parameters.Length - 1 ? ")" : ",");
+ WriteParameter(parameters[index]);
if (index != parameters.Length - 1)
+ WriteLine(",");
+ else
NewLine();
}
Unindent();
+ Write(')');
}
CodeWriter WriteParameter(ParameterDeclarationOptions parameter)
@@ -2226,12 +3103,16 @@ void WriteMemberModifiers(
bool isAbstract,
bool isVirtual,
bool isOverride,
- bool isSealed
+ bool isSealed,
+ bool isReadOnly = false,
+ bool isRequired = false
)
{
if (accessibility is { } value)
WriteAccessibility(value).Write(' ');
- WriteIf(isStatic, "static ")
+ WriteIf(isRequired, "required ")
+ .WriteIf(isReadOnly, "readonly ")
+ .WriteIf(isStatic, "static ")
.WriteIf(isSealed, "sealed ")
.WriteIf(isAbstract, "abstract ")
.WriteIf(isVirtual, "virtual ")
@@ -2246,7 +3127,8 @@ CodeWriter WritePropertyHeader(PropertyDeclarationOptions declaration)
declaration.IsAbstract,
declaration.IsVirtual,
declaration.IsOverride,
- declaration.IsSealed
+ declaration.IsSealed,
+ isRequired: declaration.IsRequired
);
return WriteTypeReference(declaration.Type).Write(' ').Write(declaration.Name);
}
@@ -2313,9 +3195,9 @@ int CurrentLineLength
{
if (_builder[index] == NewLineCharacter)
break;
- length += _builder[index] == IndentCharacter ? IndentDisplayWidth : 1;
+ length += _builder[index] == '\t' ? _indentationSize : 1;
}
- return length + (_atLineStart ? _indentLevel * IndentDisplayWidth : 0);
+ return length + (_atLineStart ? _indentLevel * _indentationSize : 0);
}
}
@@ -2329,7 +3211,11 @@ BlockScope TrackOpenBlockScope(
return new BlockScope(
this,
closingSeparator,
- OpenScope("block", header, new StackTrace(1, fNeedFileInfo: true).ToString()),
+ OpenScope(
+ "block",
+ header,
+ TracksOpenScopes ? new StackTrace(1, fNeedFileInfo: true).ToString() : string.Empty
+ ),
(int)completedItem,
itemIndent
);
@@ -2593,6 +3479,12 @@ static void ValidateTypeDeclarationModifiers(TypeDeclarationOptions declaration,
"Only struct and record struct declarations can be readonly.",
nameof(declaration)
);
+
+ if (declaration.IsRefStruct && declaration.Kind != TypeDeclarationKind.Struct)
+ throw new ArgumentException("Only struct declarations can be ref structs.", nameof(declaration));
+
+ if (declaration.IsRefStruct && declaration.BaseType is { IsEmpty: false })
+ throw new ArgumentException("A ref struct cannot specify a base type.", nameof(declaration));
}
static void ValidateAdditionalTypeKindOptions(TypeDeclarationOptions declaration, bool supportsPrimaryConstructor)
@@ -2710,10 +3602,51 @@ static void ValidateMethodDeclaration(MethodDeclarationOptions declaration)
"Method parameters cannot contain null or whitespace values.",
nameof(declaration)
);
+ if (declaration.IsReadOnly && declaration.IsStatic)
+ throw new ArgumentException("A readonly method cannot also be static.", nameof(declaration));
if (declaration.IsAbstract && declaration.ExpressionBody is not null)
throw new ArgumentException("An abstract method cannot have an expression body.", nameof(declaration));
}
+ static void ValidateOperatorDeclaration(OperatorDeclarationOptions declaration)
+ {
+ ValidateRequired(declaration.OperatorToken, "Operator token", nameof(declaration));
+ ValidateTypeReference(declaration.ReturnType, nameof(declaration));
+ ValidateMemberModifiers(
+ declaration.Accessibility,
+ isAbstract: false,
+ isVirtual: false,
+ isOverride: false,
+ isSealed: false,
+ nameof(declaration)
+ );
+ switch (declaration.Kind)
+ {
+ case OperatorDeclarationKind.Binary:
+ ValidateParameters(
+ [declaration.Left, declaration.Right],
+ "Operator parameters cannot contain null or whitespace values.",
+ nameof(declaration)
+ );
+ break;
+
+ case OperatorDeclarationKind.Unary:
+ case OperatorDeclarationKind.ImplicitConversion:
+ case OperatorDeclarationKind.ExplicitConversion:
+ ValidateParameters(
+ [declaration.Left],
+ "Operator parameters cannot contain null or whitespace values.",
+ nameof(declaration)
+ );
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(declaration));
+ }
+
+ ValidateAttributes(declaration.Attributes, nameof(declaration));
+ }
+
static void ValidatePropertyDeclaration(PropertyDeclarationOptions declaration)
{
ValidateRequired(declaration.Name, "Property name", nameof(declaration));
@@ -2740,6 +3673,35 @@ static void ValidatePropertyDeclaration(PropertyDeclarationOptions declaration)
);
if (declaration.IsAbstract && declaration.ExpressionBody is not null)
throw new ArgumentException("An abstract property cannot have an expression body.", nameof(declaration));
+ if (declaration.IsFieldBacked && declaration.ExpressionBody is not null)
+ throw new ArgumentException(
+ "A field-keyword property cannot have an expression body.",
+ nameof(declaration)
+ );
+ if (declaration.IsFieldBacked && declaration.Initializer is not null)
+ throw new ArgumentException("A field-keyword property cannot have an initializer.", nameof(declaration));
+ }
+
+ static void ValidateIndexerDeclaration(IndexerDeclarationOptions declaration)
+ {
+ ValidateTypeReference(declaration.Type, nameof(declaration));
+ ValidateMemberModifiers(
+ declaration.Accessibility,
+ declaration.IsAbstract,
+ declaration.IsVirtual,
+ declaration.IsOverride,
+ declaration.IsSealed,
+ nameof(declaration)
+ );
+ ValidateParameters(
+ declaration.Parameters,
+ "Indexer parameters cannot contain null or whitespace values.",
+ nameof(declaration)
+ );
+ if (declaration.ExpressionBody is not null && (declaration.HasSetter || declaration.IsInitOnly))
+ throw new ArgumentException("An expression-bodied indexer cannot have a setter.", nameof(declaration));
+ if (declaration.IsAbstract && declaration.ExpressionBody is not null)
+ throw new ArgumentException("An abstract indexer cannot have an expression body.", nameof(declaration));
}
static void ValidateFieldDeclaration(FieldDeclarationOptions declaration)
@@ -2760,6 +3722,12 @@ static void ValidateFieldDeclaration(FieldDeclarationOptions declaration)
throw new ArgumentException("A field cannot be both readonly and volatile.", nameof(declaration));
if (declaration.IsConst && declaration.Initializer is null)
throw new ArgumentException("A const field requires an initializer.", nameof(declaration));
+ if (declaration.IsRefField && declaration.IsConst)
+ throw new ArgumentException("A ref field cannot be const.", nameof(declaration));
+ if (declaration.IsRefField && declaration.Initializer is not null)
+ throw new ArgumentException("A ref field cannot have an initializer.", nameof(declaration));
+ if (declaration.IsRefField && declaration.IsStatic)
+ throw new ArgumentException("A ref field cannot be static.", nameof(declaration));
}
static void ValidateEnumFieldDeclaration(EnumFieldDeclarationOptions declaration)
diff --git a/src/src/SourceGeneratorShared/ConstructorDeclarationOptions.cs b/src/src/SourceGeneratorShared/ConstructorDeclarationOptions.cs
index ea25d82..1311b3f 100644
--- a/src/src/SourceGeneratorShared/ConstructorDeclarationOptions.cs
+++ b/src/src/SourceGeneratorShared/ConstructorDeclarationOptions.cs
@@ -22,13 +22,17 @@ public ConstructorDeclarationOptions(string name, TypeDeclarationAccessibility?
Accessibility = accessibility;
}
- /// Initializes a constructor declaration from its containing type.
+ ///
+ /// Initializes a constructor declaration from its containing type.
+ ///
/// The containing type. Only its unqualified declaration name is used.
/// The optional accessibility modifier, or to omit accessibility.
public ConstructorDeclarationOptions(TypeIdentity type, TypeDeclarationAccessibility? accessibility = null)
: this(type.AsTypeReference(), accessibility) { }
- /// Initializes a constructor declaration from its containing type reference.
+ ///
+ /// Initializes a constructor declaration from its containing type reference.
+ ///
/// The containing type reference.
/// The optional accessibility modifier, or to omit accessibility.
public ConstructorDeclarationOptions(TypeReference reference, TypeDeclarationAccessibility? accessibility = null)
@@ -37,7 +41,9 @@ public ConstructorDeclarationOptions(TypeReference reference, TypeDeclarationAcc
Accessibility = accessibility;
}
- /// Gets the structured containing type reference.
+ ///
+ /// Gets the structured containing type reference.
+ ///
public TypeReference Reference { get; }
///
@@ -45,15 +51,25 @@ public ConstructorDeclarationOptions(TypeReference reference, TypeDeclarationAcc
///
public TypeDeclarationAccessibility? Accessibility { get; init; }
- /// Gets whether a static constructor is emitted.
- /// Static constructors cannot declare parameters or an initializer.
+ ///
+ /// Gets whether a static constructor is emitted.
+ ///
+ ///
+ /// Static constructors cannot declare parameters or an initializer.
+ ///
public bool IsStatic { get; init; }
- /// Gets the constructor parameters.
- /// Each entry is emitted verbatim as a complete parameter declaration.
+ ///
+ /// Gets the constructor parameters.
+ ///
+ ///
+ /// Each entry is emitted verbatim as a complete parameter declaration.
+ ///
public ImmutableArray Parameters { get; init; }
- /// Gets attributes applied to the constructor.
+ ///
+ /// Gets attributes applied to the constructor.
+ ///
public ImmutableArray Attributes { get; init; }
///
@@ -61,7 +77,9 @@ public ConstructorDeclarationOptions(TypeReference reference, TypeDeclarationAcc
///
public bool WriteParametersOnSeparateLines { get; init; }
- /// Gets the optional constructor initializer without the leading colon.
+ ///
+ /// Gets the optional constructor initializer without the leading colon.
+ ///
/// base(connectionString) or this("Default").
public string? Initializer { get; init; }
diff --git a/src/src/SourceGeneratorShared/EnumFieldDeclarationOptions.cs b/src/src/SourceGeneratorShared/EnumFieldDeclarationOptions.cs
index 8da7000..53b7cfb 100644
--- a/src/src/SourceGeneratorShared/EnumFieldDeclarationOptions.cs
+++ b/src/src/SourceGeneratorShared/EnumFieldDeclarationOptions.cs
@@ -2,10 +2,14 @@
namespace Purview.SourceGeneratorFramework;
-/// Describes a field in a generated enum declaration.
+///
+/// Describes a field in a generated enum declaration.
+///
public readonly record struct EnumFieldDeclarationOptions
{
- /// Initializes an enum field declaration.
+ ///
+ /// Initializes an enum field declaration.
+ ///
/// The enum field name.
///
/// The enum field value. Strings are emitted as C# expressions; other values are
@@ -21,7 +25,9 @@ public EnumFieldDeclarationOptions(string fieldName, object fieldValue, params s
FieldValue = fieldValue;
}
- /// Initializes an enum field declaration.
+ ///
+ /// Initializes an enum field declaration.
+ ///
/// The enum field name.
/// The lines written in the field's XML summary block.
public EnumFieldDeclarationOptions(string fieldName, params string[] xmlSummary)
@@ -33,7 +39,9 @@ public EnumFieldDeclarationOptions(string fieldName, params string[] xmlSummary)
XmlSummary = [.. xmlSummary ?? []];
}
- /// Gets the enum field name.
+ ///
+ /// Gets the enum field name.
+ ///
public string FieldName { get; }
///
@@ -42,9 +50,13 @@ public EnumFieldDeclarationOptions(string fieldName, params string[] xmlSummary)
///
public object? FieldValue { get; }
- /// Gets the lines written in the field's XML summary block.
+ ///
+ /// Gets the lines written in the field's XML summary block.
+ ///
public ImmutableArray XmlSummary { get; init; } = [];
- /// Gets the attributes applied to the enum field.
+ ///
+ /// Gets the attributes applied to the enum field.
+ ///
public ImmutableArray Attributes { get; init; } = [];
}
diff --git a/src/src/SourceGeneratorShared/GenerationContext.cs b/src/src/SourceGeneratorShared/GenerationContext.cs
index ed1f436..4db30d3 100644
--- a/src/src/SourceGeneratorShared/GenerationContext.cs
+++ b/src/src/SourceGeneratorShared/GenerationContext.cs
@@ -6,7 +6,9 @@ namespace Purview.SourceGeneratorFramework;
/// Provides execution services for source generation, including the compilation, immutable
/// settings, optional logging, and symbol-resolution helpers.
///
-/// Initializes a generation context.
+///
+/// Initializes a generation context.
+///
public sealed class GenerationContext(
TCapabilities Capabilities,
GenerationSettings Settings,
@@ -19,7 +21,9 @@ public sealed class GenerationContext(
///
public TCapabilities Capabilities { get; } = Capabilities ?? throw new ArgumentNullException(nameof(Capabilities));
- /// Gets the immutable generation settings.
+ ///
+ /// Gets the immutable generation settings.
+ ///
public GenerationSettings Settings { get; } = Settings ?? throw new ArgumentNullException(nameof(Settings));
///
@@ -28,7 +32,9 @@ public sealed class GenerationContext(
///
public ISourceGenLogger? Logger { get; } = Logger;
- /// Creates a new independently owned code writer.
+ ///
+ /// Creates a new independently owned code writer.
+ ///
public CodeWriter CreateCodeWriter() => new(Settings, throwOnUnclosedScopes: Settings.ValidateCodeWriterScopes);
///
diff --git a/src/src/SourceGeneratorShared/GenerationSettings.cs b/src/src/SourceGeneratorShared/GenerationSettings.cs
index 8440c3e..6ec7910 100644
--- a/src/src/SourceGeneratorShared/GenerationSettings.cs
+++ b/src/src/SourceGeneratorShared/GenerationSettings.cs
@@ -1,9 +1,15 @@
+using Microsoft.CodeAnalysis.CSharp;
+
namespace Purview.SourceGeneratorFramework;
-/// Describes immutable settings shared by a source-generation operation.
+///
+/// Describes immutable settings shared by a source-generation operation.
+///
public sealed record GenerationSettings
{
- /// Initializes source-generation settings.
+ ///
+ /// Initializes source-generation settings.
+ ///
/// The name of the generator.
/// The version of the generator. If null, defaults to "1.0.0.0".
/// An optional MSBuild property name that disables the generator when set to true.
@@ -21,13 +27,19 @@ public GenerationSettings(
DisabledSourceGenMSBuildProperty = disabledSourceGenMSBuildProperty;
}
- /// Gets the source generator name propagated to code writers.
+ ///
+ /// Gets the source generator name propagated to code writers.
+ ///
public string GeneratorName { get; }
- /// Gets the source generator version propagated to code writers.
+ ///
+ /// Gets the source generator version propagated to code writers.
+ ///
public string GeneratorVersion { get; }
- /// Gets the optional MSBuild property name that disables the generator when set to true.
+ ///
+ /// Gets the optional MSBuild property name that disables the generator when set to true.
+ ///
public string? DisabledSourceGenMSBuildProperty { get; }
///
@@ -45,15 +57,45 @@ public GenerationSettings(
///
public bool? IsNullableContextEnabled { get; init; }
- /// Gets whether created code writers validate undisposed scopes.
+ ///
+ /// Gets whether created code writers validate undisposed scopes.
+ ///
public bool ValidateCodeWriterScopes { get; init; }
- /// Gets whether the source generator is disabled by build configuration.
+ ///
+ /// Gets whether the source generator is disabled by build configuration.
+ ///
public bool IsSourceGeneratorDisabled { get; init; }
- /// Gets whether source-generator logging is active for this generation context.
+ ///
+ /// Gets whether source-generator logging is active for this generation context.
+ ///
public bool IsLoggingEnabled { get; init; }
+ ///
+ /// Gets how generated code is indented. The default is .
+ ///
+ public IndentationStyle IndentationStyle { get; init; } = IndentationStyle.Tabs;
+
+ ///
+ /// Gets the indentation width: the number of spaces used per level when
+ /// is , or the display width of a
+ /// single tab when it is . The default is 4.
+ ///
+ public int IndentationSize { get; init; } = 4;
+
+ ///
+ /// Gets the maximum line length that drives inline-versus-multiline wrapping heuristics. The default is 100.
+ ///
+ public int MaximumLineLength { get; init; } = 100;
+
+ ///
+ /// Gets the C# language version of the target compilation, when it is known. Generators can use this to
+ /// gate emitted features such as primary constructors or collection expressions. It is seeded from the
+ /// consuming project's LangVersion when available.
+ ///
+ public LanguageVersion? LanguageVersion { get; init; }
+
///
/// Creates a new generation settings instance for the specified generator type, using the type name and assembly version.
///
diff --git a/src/src/SourceGeneratorShared/GeneratorResult.cs b/src/src/SourceGeneratorShared/GeneratorResult.cs
index e697b5c..da66473 100644
--- a/src/src/SourceGeneratorShared/GeneratorResult.cs
+++ b/src/src/SourceGeneratorShared/GeneratorResult.cs
@@ -12,7 +12,9 @@ public readonly record struct GeneratorResult
///
/// Gets the value of the generator result. If the result is a failure, this will be null or default(T).
///
- /// This value can be null or default(T) if the result is a failure.
+ ///
+ /// This value can be null or default(T) if the result is a failure.
+ ///
public T Value { get; private init; }
///
diff --git a/src/src/SourceGeneratorShared/Helpers/IncrementalPipeline.cs b/src/src/SourceGeneratorShared/Helpers/IncrementalPipeline.cs
index 9a1d9ab..8f362e2 100644
--- a/src/src/SourceGeneratorShared/Helpers/IncrementalPipeline.cs
+++ b/src/src/SourceGeneratorShared/Helpers/IncrementalPipeline.cs
@@ -107,6 +107,7 @@ public static IncrementalValueProvider<
IsLoggingEnabled = logger is not null,
IsNullableContextEnabled =
settings.IsNullableContextEnabled ?? IsNullableContextEnabled(compilation),
+ LanguageVersion = configuration.LanguageVersion ?? settings.LanguageVersion,
};
var capabilities = factory(compilation, resolvedSettings, logger, cancellationToken);
@@ -127,6 +128,7 @@ public static IncrementalValueProvider<
if (compilation is not CSharpCompilation csharpCompilation)
return null;
+ // Nullable annotations are enabled when the compilation's nullable context options are either
return csharpCompilation.Options.NullableContextOptions
is NullableContextOptions.Annotations
or NullableContextOptions.Enable;
@@ -152,6 +154,15 @@ out var loggingEnabledValue
SourceGeneratorBuildProperties.LoggingSessionId,
out var loggingSessionId
);
+ if (
+ !options.GlobalOptions.TryGetValue(
+ SourceGeneratorBuildProperties.LanguageVersion,
+ out var languageVersionValue
+ )
+ )
+ {
+ options.GlobalOptions.TryGetValue("build_property.LangVersion", out languageVersionValue);
+ }
string? disabledValue = null;
if (!string.IsNullOrWhiteSpace(disablePropertyName))
@@ -170,12 +181,18 @@ out var loggingSessionId
&& validateScopes,
IsSourceGeneratorDisabled: bool.TryParse(disabledValue, out var isDisabled) && isDisabled,
IsLoggingEnabled: bool.TryParse(loggingEnabledValue, out var loggingEnabled) && loggingEnabled,
- LoggingSessionId: loggingSessionId
+ LoggingSessionId: loggingSessionId,
+ LanguageVersion: TryParseLanguageVersion(languageVersionValue)
);
}
)
.WithTrackingName("GetGenerationConfiguration");
+ static LanguageVersion? TryParseLanguageVersion(string? value) =>
+ string.IsNullOrWhiteSpace(value) || !Enum.TryParse(value, ignoreCase: true, out LanguageVersion parsed)
+ ? null
+ : parsed;
+
///
/// Creates a values provider for syntax nodes annotated with a specific attribute.
///
@@ -202,6 +219,7 @@ readonly record struct GenerationConfiguration(
bool ValidateCodeWriterScopes,
bool IsSourceGeneratorDisabled,
bool IsLoggingEnabled,
- string? LoggingSessionId
+ string? LoggingSessionId,
+ LanguageVersion? LanguageVersion
);
}
diff --git a/src/src/SourceGeneratorShared/IndentationStyle.cs b/src/src/SourceGeneratorShared/IndentationStyle.cs
new file mode 100644
index 0000000..d31fb7e
--- /dev/null
+++ b/src/src/SourceGeneratorShared/IndentationStyle.cs
@@ -0,0 +1,17 @@
+namespace Purview.SourceGeneratorFramework;
+
+///
+/// Identifies how generated code is indented.
+///
+public enum IndentationStyle
+{
+ ///
+ /// Indents with tab characters. This is the default.
+ ///
+ Tabs,
+
+ ///
+ /// Indents with the configured number of space characters.
+ ///
+ Spaces,
+}
diff --git a/src/src/SourceGeneratorShared/IndexerDeclarationOptions.cs b/src/src/SourceGeneratorShared/IndexerDeclarationOptions.cs
new file mode 100644
index 0000000..cfb33fa
--- /dev/null
+++ b/src/src/SourceGeneratorShared/IndexerDeclarationOptions.cs
@@ -0,0 +1,104 @@
+using System.Collections.Immutable;
+
+namespace Purview.SourceGeneratorFramework;
+
+///
+/// Describes a generated indexer declaration.
+///
+public readonly record struct IndexerDeclarationOptions
+{
+ ///
+ /// Creates an indexer declaration.
+ ///
+ /// The indexer element type.
+ /// The indexer parameters.
+ public IndexerDeclarationOptions(TypeReference type, params ParameterDeclarationOptions[] parameters)
+ {
+ if (type.IsNullOrEmpty())
+ throw new ArgumentException("Indexer type cannot be empty.", nameof(type));
+
+ Type = type;
+ Parameters = parameters is null ? [] : [.. parameters];
+ }
+
+ ///
+ /// Gets the indexer element type.
+ ///
+ public TypeReference Type { get; }
+
+ ///
+ /// Gets the indexer parameters.
+ ///
+ public ImmutableArray Parameters { get; }
+
+ ///
+ /// Gets the optional accessibility modifier.
+ ///
+ public TypeDeclarationAccessibility? Accessibility { get; init; }
+
+ ///
+ /// Gets whether the indexer is static.
+ ///
+ public bool IsStatic { get; init; }
+
+ ///
+ /// Gets whether the indexer is abstract.
+ ///
+ public bool IsAbstract { get; init; }
+
+ ///
+ /// Gets whether the indexer is virtual.
+ ///
+ public bool IsVirtual { get; init; }
+
+ ///
+ /// Gets whether the indexer is an override.
+ ///
+ public bool IsOverride { get; init; }
+
+ ///
+ /// Gets whether the indexer is sealed.
+ ///
+ public bool IsSealed { get; init; }
+
+ ///
+ /// Gets whether a getter is emitted. The default is .
+ ///
+ public bool HasGetter { get; init; } = true;
+
+ ///
+ /// Gets whether a setter or init accessor is emitted.
+ ///
+ public bool HasSetter { get; init; }
+
+ ///
+ /// Gets whether the setter is emitted as an init accessor.
+ ///
+ public bool IsInitOnly { get; init; }
+
+ ///
+ /// Gets optional getter accessibility.
+ ///
+ public TypeDeclarationAccessibility? GetterAccessibility { get; init; }
+
+ ///
+ /// Gets optional setter accessibility.
+ ///
+ public TypeDeclarationAccessibility? SetterAccessibility { get; init; }
+
+ ///
+ /// Gets an optional expression body without the leading =>.
+ ///
+ public string? ExpressionBody { get; init; }
+
+ ///
+ /// Gets attributes applied to the indexer.
+ ///
+ public ImmutableArray Attributes { get; init; }
+
+ ///
+ /// Gets whether to emit generated attributes. When , the value is inherited from
+ /// .
+ ///
+ public bool? IncludeGeneratedAttributes { get; init; }
+}
diff --git a/src/src/SourceGeneratorShared/Logging/SourceGenLogging.cs b/src/src/SourceGeneratorShared/Logging/SourceGenLogging.cs
index ff2a213..a5b87c2 100644
--- a/src/src/SourceGeneratorShared/Logging/SourceGenLogging.cs
+++ b/src/src/SourceGeneratorShared/Logging/SourceGenLogging.cs
@@ -3,12 +3,16 @@
namespace Purview.SourceGeneratorFramework.Logging;
-/// Registers output sinks for isolated source-generator logging sessions.
+///
+/// Registers output sinks for isolated source-generator logging sessions.
+///
public static class SourceGenLogging
{
static readonly ConcurrentDictionary> Sinks = new();
- /// Registers a sink for a source-generator logging session.
+ ///
+ /// Registers a sink for a source-generator logging session.
+ ///
public static IDisposable RegisterSink(string sessionId, Action sink)
{
if (string.IsNullOrWhiteSpace(sessionId))
diff --git a/src/src/SourceGeneratorShared/MemberDeclarationOptions.cs b/src/src/SourceGeneratorShared/MemberDeclarationOptions.cs
index d0be63f..e0665bf 100644
--- a/src/src/SourceGeneratorShared/MemberDeclarationOptions.cs
+++ b/src/src/SourceGeneratorShared/MemberDeclarationOptions.cs
@@ -2,10 +2,14 @@
namespace Purview.SourceGeneratorFramework;
-/// Describes a generated method declaration.
+///
+/// Describes a generated method declaration.
+///
public readonly record struct MethodDeclarationOptions
{
- /// Creates a method declaration.
+ ///
+ /// Creates a method declaration.
+ ///
/// The method name.
/// The return type. The default is void.
/// The optional accessibility.
@@ -23,58 +27,98 @@ public MethodDeclarationOptions(
Accessibility = accessibility;
}
- /// Creates a method declaration with the set to void.
+ ///
+ /// Creates a method declaration with the set to void.
+ ///
/// The method name.
/// The optional accessibility.
public MethodDeclarationOptions(string name, TypeDeclarationAccessibility? accessibility = null)
: this(name, PurviewTypeLibrary.System.Void, accessibility) { }
- /// Gets the method name.
+ ///
+ /// Gets the method name.
+ ///
public string Name { get; }
- /// Gets the return type.
+ ///
+ /// Gets the return type.
+ ///
public TypeReference ReturnType { get; }
- /// Gets the optional accessibility.
+ ///
+ /// Gets the optional accessibility.
+ ///
public TypeDeclarationAccessibility? Accessibility { get; init; }
- /// Gets whether the method is static.
+ ///
+ /// Gets whether the method is static.
+ ///
public bool IsStatic { get; init; }
- /// Gets whether the method is partial.
+ ///
+ /// Gets whether the method is partial.
+ ///
public bool IsPartial { get; init; }
- /// Gets whether the method is abstract.
+ ///
+ /// Gets whether the method is abstract.
+ ///
public bool IsAbstract { get; init; }
- /// Gets whether the method is virtual.
+ ///
+ /// Gets whether the method is virtual.
+ ///
public bool IsVirtual { get; init; }
- /// Gets whether the method is an override.
+ ///
+ /// Gets whether the method is an override.
+ ///
public bool IsOverride { get; init; }
- /// Gets whether the method is sealed.
+ ///
+ /// Gets whether the method is sealed.
+ ///
public bool IsSealed { get; init; }
- /// Gets whether the method is asynchronous.
+ ///
+ /// Gets whether the method is asynchronous.
+ ///
public bool IsAsync { get; init; }
- /// Gets whether the method is unsafe.
+ ///
+ /// Gets whether the method is unsafe.
+ ///
public bool IsUnsafe { get; init; }
- /// Gets the complete parameter declarations.
+ ///
+ /// Gets whether the method is emitted as a readonly instance method, which is only valid
+ /// on members of struct or record struct declarations.
+ ///
+ public bool IsReadOnly { get; init; }
+
+ ///
+ /// Gets the complete parameter declarations.
+ ///
public ImmutableArray Parameters { get; init; }
- /// Gets attributes applied to the method.
+ ///
+ /// Gets attributes applied to the method.
+ ///
public ImmutableArray Attributes { get; init; }
- /// Gets attributes applied to the return value.
+ ///
+ /// Gets attributes applied to the return value.
+ ///
public ImmutableArray ReturnAttributes { get; init; }
- /// Gets generic parameters and constraints.
+ ///
+ /// Gets generic parameters and constraints.
+ ///
public ImmutableArray GenericTypes { get; init; }
- /// Gets an optional expression body without the leading =>.
+ ///
+ /// Gets an optional expression body without the leading =>.
+ ///
public string? ExpressionBody { get; init; }
///
@@ -85,10 +129,14 @@ public MethodDeclarationOptions(string name, TypeDeclarationAccessibility? acces
public bool? IncludeGeneratedAttributes { get; init; }
}
-/// Describes a generated property declaration.
+///
+/// Describes a generated property declaration.
+///
public readonly record struct PropertyDeclarationOptions
{
- /// Creates a property declaration.
+ ///
+ /// Creates a property declaration.
+ ///
public PropertyDeclarationOptions(
string name,
TypeReference type,
@@ -100,34 +148,60 @@ public PropertyDeclarationOptions(
Accessibility = accessibility;
}
- /// Gets the property name.
+ ///
+ /// Gets the property name.
+ ///
public string Name { get; }
- /// Gets the property type.
+ ///
+ /// Gets the property type.
+ ///
public TypeReference Type { get; }
- /// Gets the optional accessibility.
+ ///
+ /// Gets the optional accessibility.
+ ///
public TypeDeclarationAccessibility? Accessibility { get; init; }
- /// Gets whether the property is static.
+ ///
+ /// Gets whether the property is static.
+ ///
public bool IsStatic { get; init; }
- /// Gets whether the property is abstract.
+ ///
+ /// Gets whether the required modifier is emitted, marking the property as required at
+ /// object-initializer time (C# 11+).
+ ///
+ public bool IsRequired { get; init; }
+
+ ///
+ /// Gets whether the property is abstract.
+ ///
public bool IsAbstract { get; init; }
- /// Gets whether the property is virtual.
+ ///
+ /// Gets whether the property is virtual.
+ ///
public bool IsVirtual { get; init; }
- /// Gets whether the property is an override.
+ ///
+ /// Gets whether the property is an override.
+ ///
public bool IsOverride { get; init; }
- /// Gets whether the property is sealed.
+ ///
+ /// Gets whether the property is sealed.
+ ///
public bool IsSealed { get; init; }
- /// Gets whether a getter is emitted. The default is .
+ ///
+ /// Gets whether a getter is emitted. The default is .
+ ///
public bool HasGetter { get; init; } = true;
- /// Gets whether a setter or init accessor is emitted.
+ ///
+ /// Gets whether a setter or init accessor is emitted.
+ ///
public bool HasSetter { get; init; }
///
@@ -136,19 +210,38 @@ public PropertyDeclarationOptions(
///
public bool IsInitOnly { get; init; }
- /// Gets optional getter accessibility.
+ ///
+ /// Gets whether the property is emitted as a C# 14 field-keyword semi-auto property whose accessors
+ /// reference the implicit backing field, such as get => field; set => field = value;.
+ ///
+ ///
+ /// Incompatible with , , and accessor bodies.
+ ///
+ public bool IsFieldBacked { get; init; }
+
+ ///
+ /// Gets optional getter accessibility.
+ ///
public TypeDeclarationAccessibility? GetterAccessibility { get; init; }
- /// Gets optional setter accessibility.
+ ///
+ /// Gets optional setter accessibility.
+ ///
public TypeDeclarationAccessibility? SetterAccessibility { get; init; }
- /// Gets an optional expression body without the leading =>.
+ ///
+ /// Gets an optional expression body without the leading =>.
+ ///
public string? ExpressionBody { get; init; }
- /// Gets an optional initializer without the leading equals sign.
+ ///
+ /// Gets an optional initializer without the leading equals sign.
+ ///
public string? Initializer { get; init; }
- /// Gets attributes applied to the property.
+ ///
+ /// Gets attributes applied to the property.
+ ///
public ImmutableArray Attributes { get; init; }
///
@@ -159,10 +252,14 @@ public PropertyDeclarationOptions(
public bool? IncludeGeneratedAttributes { get; init; }
}
-/// Describes a generated field declaration.
+///
+/// Describes a generated field declaration.
+///
public readonly record struct FieldDeclarationOptions
{
- /// Creates a field declaration.
+ ///
+ /// Creates a field declaration.
+ ///
public FieldDeclarationOptions(string name, TypeReference type, TypeDeclarationAccessibility? accessibility = null)
{
Name = name;
@@ -170,31 +267,61 @@ public FieldDeclarationOptions(string name, TypeReference type, TypeDeclarationA
Accessibility = accessibility;
}
- /// Gets the field name.
+ ///
+ /// Gets the field name.
+ ///
public string Name { get; }
- /// Gets the field type.
+ ///
+ /// Gets the field type.
+ ///
public TypeReference Type { get; }
- /// Gets the optional accessibility.
+ ///
+ /// Gets the optional accessibility.
+ ///
public TypeDeclarationAccessibility? Accessibility { get; init; }
- /// Gets whether the field is static.
+ ///
+ /// Gets whether the field is static.
+ ///
public bool IsStatic { get; init; }
- /// Gets whether the field is readonly.
+ ///
+ /// Gets whether the required modifier is emitted, marking the field as required at
+ /// object-initializer time (C# 11+).
+ ///
+ public bool IsRequired { get; init; }
+
+ ///
+ /// Gets whether the field is readonly.
+ ///
public bool IsReadOnly { get; init; }
- /// Gets whether the field is constant.
+ ///
+ /// Gets whether the ref modifier is emitted before the field type, producing a ref field.
+ /// Only valid in ref struct declarations and incompatible with constants.
+ ///
+ public bool IsRefField { get; init; }
+
+ ///
+ /// Gets whether the field is constant.
+ ///
public bool IsConst { get; init; }
- /// Gets whether the field is volatile.
+ ///
+ /// Gets whether the field is volatile.
+ ///
public bool IsVolatile { get; init; }
- /// Gets an optional initializer without the leading equals sign.
+ ///
+ /// Gets an optional initializer without the leading equals sign.
+ ///
public string? Initializer { get; init; }
- /// Gets attributes applied to the field.
+ ///
+ /// Gets attributes applied to the field.
+ ///
public ImmutableArray Attributes { get; init; }
///
diff --git a/src/src/SourceGeneratorShared/MethodCallArgumentOptions.cs b/src/src/SourceGeneratorShared/MethodCallArgumentOptions.cs
index baf3e62..39f8070 100644
--- a/src/src/SourceGeneratorShared/MethodCallArgumentOptions.cs
+++ b/src/src/SourceGeneratorShared/MethodCallArgumentOptions.cs
@@ -1,9 +1,13 @@
namespace Purview.SourceGeneratorFramework;
-/// Describes one argument supplied to a generated method call.
+///
+/// Describes one argument supplied to a generated method call.
+///
public readonly record struct MethodCallArgumentOptions
{
- /// Creates a method-call argument from its value expression.
+ ///
+ /// Creates a method-call argument from its value expression.
+ ///
/// The argument expression or variable name.
/// An optional named-argument label.
/// The argument passing modifier.
@@ -28,13 +32,19 @@ public MethodCallArgumentOptions(
public MethodCallArgumentOptions(string value, ParameterModifier modifier)
: this(value, null, modifier) { }
- /// Gets the argument expression or variable name.
+ ///
+ /// Gets the argument expression or variable name.
+ ///
public string Value { get; }
- /// Gets an optional named-argument label.
+ ///
+ /// Gets an optional named-argument label.
+ ///
public string? Name { get; init; }
- /// Gets the argument passing modifier.
+ ///
+ /// Gets the argument passing modifier.
+ ///
public ParameterModifier Modifier { get; init; }
public static implicit operator MethodCallArgumentOptions(string value) => new(value);
diff --git a/src/src/SourceGeneratorShared/NullableDirectiveMode.cs b/src/src/SourceGeneratorShared/NullableDirectiveMode.cs
index 60e9cfb..dde0c05 100644
--- a/src/src/SourceGeneratorShared/NullableDirectiveMode.cs
+++ b/src/src/SourceGeneratorShared/NullableDirectiveMode.cs
@@ -13,9 +13,13 @@ public enum NullableDirectiveMode
///
Auto = 0,
- /// Always emits the #nullable enable directive.
+ ///
+ /// Always emits the #nullable enable directive.
+ ///
Always = 1,
- /// Never emits the #nullable enable directive.
+ ///
+ /// Never emits the #nullable enable directive.
+ ///
Disable = 2,
}
diff --git a/src/src/SourceGeneratorShared/ObjectCreationOptions.cs b/src/src/SourceGeneratorShared/ObjectCreationOptions.cs
index ac78ae1..e67155a 100644
--- a/src/src/SourceGeneratorShared/ObjectCreationOptions.cs
+++ b/src/src/SourceGeneratorShared/ObjectCreationOptions.cs
@@ -2,10 +2,21 @@
namespace Purview.SourceGeneratorFramework;
-/// Describes a generated object-creation expression.
+///
+/// Describes one member assignment in a generated object-initializer expression.
+///
+/// The member name.
+/// The assigned value expression.
+public readonly record struct ObjectInitializerMemberOptions(string Name, string? Value);
+
+///
+/// Describes a generated object-creation expression.
+///
public readonly record struct ObjectCreationOptions
{
- /// Creates an object-creation expression.
+ ///
+ /// Creates an object-creation expression.
+ ///
/// The type to instantiate.
/// The constructor arguments; strings are implicitly supported.
public ObjectCreationOptions(TypeReference reference, params MethodCallArgumentOptions[] arguments)
@@ -17,12 +28,29 @@ public ObjectCreationOptions(TypeReference reference, params MethodCallArgumentO
Arguments = arguments is null ? [] : [.. arguments];
}
- /// Gets the type to instantiate.
+ ///
+ /// Gets the type to instantiate.
+ ///
public TypeReference Reference { get; }
- /// Gets the constructor arguments.
+ ///
+ /// Gets the constructor arguments.
+ ///
public ImmutableArray Arguments { get; }
- /// Gets whether constructor arguments are written one per line.
+ ///
+ /// Gets whether constructor arguments are written one per line.
+ ///
public bool WriteArgumentsOnSeparateLines { get; init; }
+
+ ///
+ /// Gets the object-initializer member assignments written after the constructor arguments.
+ ///
+ public ImmutableArray InitializerMembers { get; init; }
+
+ ///
+ /// Gets whether object-initializer members are written one per line with a trailing comma.
+ /// The default is .
+ ///
+ public bool WriteInitializerMembersOnSeparateLines { get; init; } = true;
}
diff --git a/src/src/SourceGeneratorShared/OperatorDeclarationOptions.cs b/src/src/SourceGeneratorShared/OperatorDeclarationOptions.cs
new file mode 100644
index 0000000..73d3148
--- /dev/null
+++ b/src/src/SourceGeneratorShared/OperatorDeclarationOptions.cs
@@ -0,0 +1,71 @@
+using System.Collections.Immutable;
+
+namespace Purview.SourceGeneratorFramework;
+
+///
+/// Identifies the shape of a generated operator declaration.
+///
+public enum OperatorDeclarationKind
+{
+ ///
+ /// A binary operator such as ==, <, or +, taking two operands.
+ ///
+ Binary,
+
+ ///
+ /// A unary operator such as - or !, taking a single operand.
+ ///
+ Unary,
+
+ ///
+ /// An implicit conversion operator.
+ ///
+ ImplicitConversion,
+
+ ///
+ /// An explicit conversion operator.
+ ///
+ ExplicitConversion,
+}
+
+///
+/// Describes a generated operator declaration.
+///
+///
+/// The operator token such as ==, <, or <=. Ignored for conversion operators.
+///
+///
+/// The operator return type: the result for binary/unary operators, or the target type for conversion operators.
+///
+///
+/// The left operand parameter, or the single source parameter for unary and conversion operators.
+///
+/// The right operand parameter. Only used for .
+/// The optional accessibility modifier, or to omit accessibility.
+///
+/// Whether the static keyword is emitted. Operators are implicitly static; the default is
+/// and matches the convention of explicit static emission.
+///
+/// An optional expression body without the leading =>.
+/// Attributes applied to the operator.
+///
+/// Whether to emit generated attributes. When , the value is inherited from
+/// .
+///
+public readonly record struct OperatorDeclarationOptions(
+ string OperatorToken,
+ TypeReference ReturnType,
+ ParameterDeclarationOptions Left,
+ ParameterDeclarationOptions Right = default,
+ TypeDeclarationAccessibility? Accessibility = null,
+ bool IsStatic = true,
+ string? ExpressionBody = null,
+ ImmutableArray Attributes = default,
+ bool? IncludeGeneratedAttributes = null
+)
+{
+ ///
+ /// Gets the operator shape, which controls how the header is emitted.
+ ///
+ public OperatorDeclarationKind Kind { get; init; } = OperatorDeclarationKind.Binary;
+}
diff --git a/src/src/SourceGeneratorShared/ParameterDeclarationOptions.cs b/src/src/SourceGeneratorShared/ParameterDeclarationOptions.cs
index 9db0200..c2f888d 100644
--- a/src/src/SourceGeneratorShared/ParameterDeclarationOptions.cs
+++ b/src/src/SourceGeneratorShared/ParameterDeclarationOptions.cs
@@ -2,10 +2,14 @@
namespace Purview.SourceGeneratorFramework;
-/// Describes a generated method, constructor, delegate, or primary-constructor parameter.
+///
+/// Describes a generated method, constructor, delegate, or primary-constructor parameter.
+///
public readonly record struct ParameterDeclarationOptions
{
- /// Creates a parameter declaration.
+ ///
+ /// Creates a parameter declaration.
+ ///
public ParameterDeclarationOptions(
string name,
TypeReference reference,
@@ -17,27 +21,43 @@ public ParameterDeclarationOptions(
Modifier = modifier;
}
- /// Gets the parameter name.
+ ///
+ /// Gets the parameter name.
+ ///
public string Name { get; }
- /// Gets the parameter type.
+ ///
+ /// Gets the parameter type.
+ ///
public TypeReference Reference { get; }
- /// Gets the parameter passing modifier.
+ ///
+ /// Gets the parameter passing modifier.
+ ///
public ParameterModifier Modifier { get; init; }
- /// Gets whether this is emitted for an extension receiver.
+ ///
+ /// Gets whether this is emitted for an extension receiver.
+ ///
public bool IsThis { get; init; }
- /// Gets whether params is emitted.
+ ///
+ /// Gets whether params is emitted.
+ ///
public bool IsParams { get; init; }
- /// Gets whether scoped is emitted.
+ ///
+ /// Gets whether scoped is emitted.
+ ///
public bool IsScoped { get; init; }
- /// Gets an optional default-value expression.
+ ///
+ /// Gets an optional default-value expression.
+ ///
public string? DefaultValue { get; init; }
- /// Gets attributes applied to the parameter.
+ ///
+ /// Gets attributes applied to the parameter.
+ ///
public ImmutableArray Attributes { get; init; }
}
diff --git a/src/src/SourceGeneratorShared/ParameterModifier.cs b/src/src/SourceGeneratorShared/ParameterModifier.cs
index 0ad6731..26b8fd7 100644
--- a/src/src/SourceGeneratorShared/ParameterModifier.cs
+++ b/src/src/SourceGeneratorShared/ParameterModifier.cs
@@ -1,20 +1,32 @@
namespace Purview.SourceGeneratorFramework;
-/// Identifies a generated parameter modifier.
+///
+/// Identifies a generated parameter modifier.
+///
public enum ParameterModifier
{
- /// No modifier.
+ ///
+ /// No modifier.
+ ///
None,
- /// The ref modifier.
+ ///
+ /// The ref modifier.
+ ///
Ref,
- /// The out modifier.
+ ///
+ /// The out modifier.
+ ///
Out,
- /// The in modifier.
+ ///
+ /// The in modifier.
+ ///
In,
- /// The ref readonly modifier.
+ ///
+ /// The ref readonly modifier.
+ ///
RefReadOnly,
}
diff --git a/src/src/SourceGeneratorShared/ResolvedTypeInformation.cs b/src/src/SourceGeneratorShared/ResolvedTypeInformation.cs
index 3163667..06076d0 100644
--- a/src/src/SourceGeneratorShared/ResolvedTypeInformation.cs
+++ b/src/src/SourceGeneratorShared/ResolvedTypeInformation.cs
@@ -12,6 +12,8 @@ public readonly record struct ResolvedTypeInformation(TypeReference Reference, A
///
/// Gets the type value object associated with the resolved type reference.
///
- /// Retrieved from
+ ///
+ /// Retrieved from
+ ///
public TypeIdentity Type => Reference.Identity;
}
diff --git a/src/src/SourceGeneratorShared/SourceGeneratorBuildProperties.cs b/src/src/SourceGeneratorShared/SourceGeneratorBuildProperties.cs
index 4a9e56e..6c3bb83 100644
--- a/src/src/SourceGeneratorShared/SourceGeneratorBuildProperties.cs
+++ b/src/src/SourceGeneratorShared/SourceGeneratorBuildProperties.cs
@@ -22,4 +22,10 @@ public static class SourceGeneratorBuildProperties
/// The MSBuild property that identifies the registered logging sink for a generator run.
///
public const string LoggingSessionId = BuildProperty + "PurviewSourceGeneratorFrameworkLoggingSessionId";
+
+ ///
+ /// The MSBuild property that carries the consuming project's LangVersion so generators can gate
+ /// emitted features. Falls back to the standard build_property.LangVersion when not configured.
+ ///
+ public const string LanguageVersion = BuildProperty + "PurviewSourceGeneratorFrameworkLanguageVersion";
}
diff --git a/src/src/SourceGeneratorShared/TypeDeclarationAccessibility.cs b/src/src/SourceGeneratorShared/TypeDeclarationAccessibility.cs
index 8b1e623..9923b0a 100644
--- a/src/src/SourceGeneratorShared/TypeDeclarationAccessibility.cs
+++ b/src/src/SourceGeneratorShared/TypeDeclarationAccessibility.cs
@@ -5,24 +5,38 @@ namespace Purview.SourceGeneratorFramework;
///
public enum TypeDeclarationAccessibility
{
- /// The public accessibility modifier.
+ ///
+ /// The public accessibility modifier.
+ ///
Public,
- /// The internal accessibility modifier.
+ ///
+ /// The internal accessibility modifier.
+ ///
Internal,
- /// The protected accessibility modifier.
+ ///
+ /// The protected accessibility modifier.
+ ///
Protected,
- /// The private accessibility modifier.
+ ///
+ /// The private accessibility modifier.
+ ///
Private,
- /// The protected internal accessibility modifier.
+ ///
+ /// The protected internal accessibility modifier.
+ ///
ProtectedInternal,
- /// The private protected accessibility modifier.
+ ///
+ /// The private protected accessibility modifier.
+ ///
PrivateProtected,
- /// The file accessibility modifier.
+ ///
+ /// The file accessibility modifier.
+ ///
File,
}
diff --git a/src/src/SourceGeneratorShared/TypeDeclarationAccessibilityExtensions.cs b/src/src/SourceGeneratorShared/TypeDeclarationAccessibilityExtensions.cs
index 7949b37..d967f00 100644
--- a/src/src/SourceGeneratorShared/TypeDeclarationAccessibilityExtensions.cs
+++ b/src/src/SourceGeneratorShared/TypeDeclarationAccessibilityExtensions.cs
@@ -15,7 +15,9 @@ public static class TypeDeclarationAccessibilityExtensions
/// The corresponding declaration accessibility, or when Roslyn reports
/// or an unknown future value.
///
- /// This method never throws for an accessibility value.
+ ///
+ /// This method never throws for an accessibility value.
+ ///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0072:Add missing cases")]
public static TypeDeclarationAccessibility? ToTypeDeclarationAccessibility(this Accessibility accessibility) =>
accessibility switch
diff --git a/src/src/SourceGeneratorShared/TypeDeclarationKind.cs b/src/src/SourceGeneratorShared/TypeDeclarationKind.cs
index c4e3406..7d587fb 100644
--- a/src/src/SourceGeneratorShared/TypeDeclarationKind.cs
+++ b/src/src/SourceGeneratorShared/TypeDeclarationKind.cs
@@ -5,24 +5,38 @@ namespace Purview.SourceGeneratorFramework;
///
public enum TypeDeclarationKind
{
- /// A class declaration.
+ ///
+ /// A class declaration.
+ ///
Class,
- /// A struct declaration.
+ ///
+ /// A struct declaration.
+ ///
Struct,
- /// A record class declaration.
+ ///
+ /// A record class declaration.
+ ///
RecordClass,
- /// A record struct declaration.
+ ///
+ /// A record struct declaration.
+ ///
RecordStruct,
- /// An interface declaration.
+ ///
+ /// An interface declaration.
+ ///
Interface,
- /// An enum declaration.
+ ///
+ /// An enum declaration.
+ ///
Enum,
- /// A delegate declaration.
+ ///
+ /// A delegate declaration.
+ ///
Delegate,
}
diff --git a/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs b/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs
index 94e3c24..daf34d3 100644
--- a/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs
+++ b/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs
@@ -82,19 +82,33 @@ public TypeDeclarationOptions(TypeIdentity type, TypeDeclarationAccessibility? a
///
public bool IsReadOnly { get; init; }
+ ///
+ /// Gets whether the ref modifier is emitted, producing a ref struct declaration.
+ /// Only valid for .
+ ///
+ public bool IsRefStruct { get; init; }
+
///
/// Gets the optional base class or base record type.
///
- /// Struct and record struct declarations cannot specify a base type.
+ ///
+ /// Struct and record struct declarations cannot specify a base type.
+ ///
public TypeReference? BaseType { get; init; }
- /// Gets the optional enum underlying integral type.
+ ///
+ /// Gets the optional enum underlying integral type.
+ ///
public TypeReference? EnumUnderlyingType { get; init; }
- /// Gets the delegate return type.
+ ///
+ /// Gets the delegate return type.
+ ///
public TypeReference? DelegateReturnType { get; init; }
- /// Gets the complete delegate parameter declarations.
+ ///
+ /// Gets the complete delegate parameter declarations.
+ ///
public ImmutableArray DelegateParameters { get; init; } = [];
///
@@ -110,7 +124,9 @@ public TypeDeclarationOptions(TypeIdentity type, TypeDeclarationAccessibility? a
///
/// Gets the primary-constructor parameters written after the type name and generic parameters.
///
- /// Each entry is emitted verbatim as a complete parameter declaration.
+ ///
+ /// Each entry is emitted verbatim as a complete parameter declaration.
+ ///
public ImmutableArray PrimaryConstructorParameters { get; init; } = [];
///
diff --git a/src/src/SourceGeneratorShared/TypeIdentity.cs b/src/src/SourceGeneratorShared/TypeIdentity.cs
index e274362..e7c67a8 100644
--- a/src/src/SourceGeneratorShared/TypeIdentity.cs
+++ b/src/src/SourceGeneratorShared/TypeIdentity.cs
@@ -279,10 +279,14 @@ public TypeIdentity(SpecialType specialType)
///
public bool IsGenericTypeDefinition => GenericArity > 0 && TypeArguments.IsDefaultOrEmpty;
- /// Gets a value indicating whether the type is nested inside another type.
+ ///
+ /// Gets a value indicating whether the type is nested inside another type.
+ ///
public bool IsNested => !ContainingTypes.IsDefaultOrEmpty;
- /// Gets a value indicating whether the type is in the global namespace.
+ ///
+ /// Gets a value indicating whether the type is in the global namespace.
+ ///
public bool IsGlobalNamespace => Namespace is null;
///
@@ -461,10 +465,14 @@ public bool Matches(ITypeSymbol? other)
///
public bool Equals(ITypeSymbol? other) => Matches(other);
- /// Determines whether the specified runtime type represents the same semantic type.
+ ///
+ /// Determines whether the specified runtime type represents the same semantic type.
+ ///
public bool Equals(Type? other) => other is not null && TryCreate(other, out var value) && Equals(value);
- /// Determines whether the specified structured reference is an unmodified reference to this type.
+ ///
+ /// Determines whether the specified structured reference is an unmodified reference to this type.
+ ///
public bool Equals(TypeReference? other) => other is not null && other.Equals(this);
///
@@ -524,7 +532,9 @@ public bool Similar(TypeIdentity other) =>
public static bool operator ==(TypeIdentity left, TypeReference? right) =>
right is not null && right.IsPlainNamedType && right.Identity.Equals(left);
- /// Negates .
+ ///
+ /// Negates .
+ ///
public static bool operator !=(TypeIdentity left, TypeReference? right) => !(left == right);
///
@@ -565,10 +575,14 @@ public override int GetHashCode()
// Composition
// ---------------------------------------------------------------------------------------------
- /// Creates the canonical source-generation type reference for this type.
+ ///
+ /// Creates the canonical source-generation type reference for this type.
+ ///
public TypeReference AsTypeReference() => new(this);
- /// Creates a nullable structured type reference.
+ ///
+ /// Creates a nullable structured type reference.
+ ///
public TypeReference MakeNullable() => AsTypeReference().Nullable();
///
@@ -595,10 +609,14 @@ public override int GetHashCode()
/// The modified type reference.
public TypeReference MakeNullable(Compilation compilation) => AsTypeReference().Nullable(compilation);
- /// Creates an array structured type reference with the specified rank.
+ ///
+ /// Creates an array structured type reference with the specified rank.
+ ///
public TypeReference MakeArray(int rank = 1) => AsTypeReference().MakeArray(rank);
- /// Creates a pointer structured type reference.
+ ///
+ /// Creates a pointer structured type reference.
+ ///
public TypeReference MakePointer() => AsTypeReference().MakePointer();
///
@@ -711,7 +729,9 @@ public TypeIdentity MakeGeneric(params TypeReference[] typeArguments)
// Factories
// ---------------------------------------------------------------------------------------------
- /// Gets an empty .
+ ///
+ /// Gets an empty .
+ ///
public static readonly TypeIdentity Empty;
///
diff --git a/src/src/SourceGeneratorShared/TypeIdentityExtensions.cs b/src/src/SourceGeneratorShared/TypeIdentityExtensions.cs
index 0617f32..dc65a8c 100644
--- a/src/src/SourceGeneratorShared/TypeIdentityExtensions.cs
+++ b/src/src/SourceGeneratorShared/TypeIdentityExtensions.cs
@@ -5,7 +5,9 @@ namespace Purview.SourceGeneratorFramework;
[EditorBrowsable(EditorBrowsableState.Never)]
public static class TypeIdentityExtensions
{
- /// Returns a fully qualified reference to a static member on the specified type.
+ ///
+ /// Returns a fully qualified reference to a static member on the specified type.
+ ///
/// The type that declares the static member.
/// The static field, property, method, or nested-type name.
/// A C# expression in the form global::Namespace.Type.Member.
diff --git a/src/src/SourceGeneratorShared/TypeModifier.cs b/src/src/SourceGeneratorShared/TypeModifier.cs
index aa15cb0..080a65f 100644
--- a/src/src/SourceGeneratorShared/TypeModifier.cs
+++ b/src/src/SourceGeneratorShared/TypeModifier.cs
@@ -5,16 +5,22 @@ namespace Purview.SourceGeneratorFramework;
///
public enum TypeModifierKind
{
- /// A nullable annotation, or a wrapper for a value type.
+ ///
+ /// A nullable annotation, or a wrapper for a value type.
+ ///
Nullable = 0,
- /// A pointer indirection.
+ ///
+ /// A pointer indirection.
+ ///
///
/// Calling this field `Pointer` results in a CA1720 warning because it is a reserved keyword in C#. The name `PointerModifier` is used instead to avoid the warning.
///
PointerModifier = 1,
- /// An array of the given rank.
+ ///
+ /// An array of the given rank.
+ ///
Array = 2,
}
@@ -31,13 +37,19 @@ public enum TypeModifierKind
///
public enum NullableModifierKind
{
- /// The value-versus-reference question is unknown.
+ ///
+ /// The value-versus-reference question is unknown.
+ ///
Unknown = 0,
- /// The modifier represents a nullable value type, such as int? or Nullable<T>.
+ ///
+ /// The modifier represents a nullable value type, such as int? or Nullable<T>.
+ ///
ValueType = 1,
- /// The modifier represents a nullable reference type annotation, such as string?.
+ ///
+ /// The modifier represents a nullable reference type annotation, such as string?.
+ ///
Reference = 2,
}
@@ -50,10 +62,14 @@ public enum NullableModifierKind
///
public readonly struct TypeModifier : IEquatable
{
- /// Gets the kind of composition step.
+ ///
+ /// Gets the kind of composition step.
+ ///
public TypeModifierKind Kind { get; init; }
- /// Gets the array rank. Only meaningful when is .
+ ///
+ /// Gets the array rank. Only meaningful when is .
+ ///
public int Rank { get; init; }
///
@@ -63,10 +79,14 @@ public enum NullableModifierKind
///
public NullableModifierKind NullableKind { get; init; }
- /// Gets a nullable modifier whose value-versus-reference classification is unknown.
+ ///
+ /// Gets a nullable modifier whose value-versus-reference classification is unknown.
+ ///
public static TypeModifier Nullable => new() { Kind = TypeModifierKind.Nullable, Rank = 0 };
- /// Gets a nullable modifier representing a nullable value type.
+ ///
+ /// Gets a nullable modifier representing a nullable value type.
+ ///
public static TypeModifier NullableValueType =>
new()
{
@@ -75,7 +95,9 @@ public enum NullableModifierKind
NullableKind = NullableModifierKind.ValueType,
};
- /// Gets a nullable modifier representing a nullable reference type annotation.
+ ///
+ /// Gets a nullable modifier representing a nullable reference type annotation.
+ ///
public static TypeModifier NullableReference =>
new()
{
@@ -84,10 +106,14 @@ public enum NullableModifierKind
NullableKind = NullableModifierKind.Reference,
};
- /// Gets a pointer modifier.
+ ///
+ /// Gets a pointer modifier.
+ ///
public static TypeModifier PointerModifier => new() { Kind = TypeModifierKind.PointerModifier, Rank = 0 };
- /// Creates an array modifier of the given rank.
+ ///
+ /// Creates an array modifier of the given rank.
+ ///
/// Thrown when is less than one.
public static TypeModifier Array(int rank = 1)
{
@@ -131,9 +157,13 @@ public override int GetHashCode()
}
}
- /// Compares modifiers by their structural shape, ignoring the render-only nullable classification.
+ ///
+ /// Compares modifiers by their structural shape, ignoring the render-only nullable classification.
+ ///
public static bool operator ==(TypeModifier left, TypeModifier right) => left.Equals(right);
- /// Compares modifiers by their structural shape, ignoring the render-only nullable classification.
+ ///
+ /// Compares modifiers by their structural shape, ignoring the render-only nullable classification.
+ ///
public static bool operator !=(TypeModifier left, TypeModifier right) => !left.Equals(right);
}
diff --git a/src/src/SourceGeneratorShared/TypeReference.cs b/src/src/SourceGeneratorShared/TypeReference.cs
index 6ada51f..85d0516 100644
--- a/src/src/SourceGeneratorShared/TypeReference.cs
+++ b/src/src/SourceGeneratorShared/TypeReference.cs
@@ -57,13 +57,19 @@ public TypeReference(TypeIdentity typeIdentity)
Modifiers = [];
}
- /// Gets a value indicating whether this reference is empty.
+ ///
+ /// Gets a value indicating whether this reference is empty.
+ ///
public bool IsEmpty => Kind == TypeReferenceKind.None;
- /// Gets what this reference refers to beneath its modifiers.
+ ///
+ /// Gets what this reference refers to beneath its modifiers.
+ ///
public TypeReferenceKind Kind { get; init; }
- /// Gets the named type, when is .
+ ///
+ /// Gets the named type, when is .
+ ///
public TypeIdentity Identity { get; init; }
///
@@ -82,13 +88,19 @@ public TypeReference(TypeIdentity typeIdentity)
///
public bool IsPlainNamedType => Kind == TypeReferenceKind.Named && Modifiers.IsDefaultOrEmpty;
- /// Gets a value indicating whether the outermost modifier is an array.
+ ///
+ /// Gets a value indicating whether the outermost modifier is an array.
+ ///
public bool IsArray => LastModifier?.Kind == TypeModifierKind.Array;
- /// Gets a value indicating whether the outermost modifier is a pointer.
+ ///
+ /// Gets a value indicating whether the outermost modifier is a pointer.
+ ///
public bool IsPointer => LastModifier?.Kind == TypeModifierKind.PointerModifier;
- /// Gets a value indicating whether the outermost modifier is a nullable annotation.
+ ///
+ /// Gets a value indicating whether the outermost modifier is a nullable annotation.
+ ///
public bool IsNullable => LastModifier?.Kind == TypeModifierKind.Nullable;
TypeModifier? LastModifier => Modifiers.IsDefaultOrEmpty ? null : Modifiers[Modifiers.Length - 1];
@@ -136,7 +148,7 @@ public string RenderFullNameForNullable(bool nullableSupported)
if (Modifiers[index].Kind != TypeModifierKind.Array)
{
var modifier = Modifiers[index];
- if (ShouldRender(modifier, nullableSupported))
+ if (ShouldRender(modifier, nullableSupported, Kind))
builder.Append(modifier.Suffix);
index++;
@@ -154,13 +166,23 @@ public string RenderFullNameForNullable(bool nullableSupported)
return builder.ToString();
}
- static bool ShouldRender(TypeModifier modifier, bool nullableSupported)
+ static bool ShouldRender(TypeModifier modifier, bool nullableSupported, TypeReferenceKind referenceKind)
{
if (modifier.Kind != TypeModifierKind.Nullable)
return true;
- // Nullable value types and unclassified annotations are always rendered, because they are part of the type identity.
- return nullableSupported || modifier.NullableKind != NullableModifierKind.Reference;
+ if (modifier.NullableKind == NullableModifierKind.Reference)
+ return nullableSupported;
+
+ if (modifier.NullableKind == NullableModifierKind.Unknown && !nullableSupported)
+ {
+ // Type parameters and dynamic are reference-like, so their unclassified annotations are invalid
+ // outside a nullable context and are elided. An unclassifiable named type is rendered
+ // conservatively so a genuine value type cannot be silently changed.
+ return referenceKind is not (TypeReferenceKind.TypeParameter or TypeReferenceKind.Dynamic);
+ }
+
+ return true;
}
///
@@ -211,7 +233,9 @@ public string RenderAttributeName
// Composition
// ---------------------------------------------------------------------------------------------
- /// Appends a nullable annotation.
+ ///
+ /// Appends a nullable annotation.
+ ///
///
/// The annotation is classified from the annotated type where possible, so a nullable value type such as
/// int? is never elided when the target compilation does not support nullable annotations, while a
@@ -255,10 +279,14 @@ public TypeReference Nullable(Compilation compilation) =>
? AppendNullable(TypeModifier.Nullable)
: this;
- /// Appends an array of the given rank.
+ ///
+ /// Appends an array of the given rank.
+ ///
public TypeReference MakeArray(int rank = 1) => Append(TypeModifier.Array(rank));
- /// Appends a pointer indirection.
+ ///
+ /// Appends a pointer indirection.
+ ///
public TypeReference MakePointer() => Append(TypeModifier.PointerModifier);
static bool ShouldComposeNullable(NullableDirectiveMode mode, bool? isNullableContextEnabled) =>
@@ -438,7 +466,9 @@ current is INamedTypeSymbol nullable
public static bool operator ==(TypeReference? left, TypeIdentity right) =>
left is not null && left.IsPlainNamedType && left.Identity.Equals(right);
- /// Negates .
+ ///
+ /// Negates .
+ ///
public static bool operator !=(TypeReference? left, TypeIdentity right) => !(left == right);
///
@@ -594,13 +624,19 @@ public override int GetHashCode()
// Factories
// ---------------------------------------------------------------------------------------------
- /// Gets the empty reference.
+ ///
+ /// Gets the empty reference.
+ ///
public static readonly TypeReference Empty = new();
- /// Gets a reference to .
+ ///
+ /// Gets a reference to .
+ ///
public static TypeReference Dynamic { get; } = new() { Kind = TypeReferenceKind.Dynamic, Modifiers = [] };
- /// Creates a reference to an open generic parameter.
+ ///
+ /// Creates a reference to an open generic parameter.
+ ///
public static TypeReference ForTypeParameter(string name)
{
if (name == null)
@@ -615,10 +651,14 @@ public static TypeReference ForTypeParameter(string name)
};
}
- /// Creates a reference from a runtime type.
+ ///
+ /// Creates a reference from a runtime type.
+ ///
public static TypeReference Create() => Create(typeof(T));
- /// Creates a reference from a runtime type.
+ ///
+ /// Creates a reference from a runtime type.
+ ///
/// Thrown when the type cannot be represented.
public static TypeReference Create(Type type)
{
@@ -632,7 +672,9 @@ public static TypeReference Create(Type type)
return value;
}
- /// Creates a reference from a type symbol.
+ ///
+ /// Creates a reference from a type symbol.
+ ///
/// Thrown when the symbol cannot be represented.
public static TypeReference Create(ITypeSymbol typeSymbol)
{
diff --git a/src/src/SourceGeneratorShared/TypeReferenceKind.cs b/src/src/SourceGeneratorShared/TypeReferenceKind.cs
index cddf150..e376f14 100644
--- a/src/src/SourceGeneratorShared/TypeReferenceKind.cs
+++ b/src/src/SourceGeneratorShared/TypeReferenceKind.cs
@@ -5,15 +5,23 @@ namespace Purview.SourceGeneratorFramework;
///
public enum TypeReferenceKind
{
- /// No type. The default, uninitialised state.
+ ///
+ /// No type. The default, uninitialised state.
+ ///
None = 0,
- /// A named type described by a .
+ ///
+ /// A named type described by a .
+ ///
Named = 1,
- /// An open generic parameter, identified by name.
+ ///
+ /// An open generic parameter, identified by name.
+ ///
TypeParameter = 2,
- /// The type.
+ ///
+ /// The type.
+ ///
Dynamic = 3,
}
diff --git a/src/src/SourceGeneratorShared/TypeSyntaxMatchingExtensions.cs b/src/src/SourceGeneratorShared/TypeSyntaxMatchingExtensions.cs
index 349fe70..0c055c1 100644
--- a/src/src/SourceGeneratorShared/TypeSyntaxMatchingExtensions.cs
+++ b/src/src/SourceGeneratorShared/TypeSyntaxMatchingExtensions.cs
@@ -218,7 +218,9 @@ public static bool MatchesDeclaredType(
///
/// Determines, without a semantic model, whether the attribute could be an application of this type.
///
- /// The Attribute suffix is optional at the application site, so both spellings are accepted.
+ ///
+ /// The Attribute suffix is optional at the application site, so both spellings are accepted.
+ ///
public static bool CouldMatchAttribute(this in TypeIdentity type, SyntaxNode? node)
{
var name = node switch
diff --git a/src/src/SourceGeneratorShared/XmlCommentWriter.cs b/src/src/SourceGeneratorShared/XmlCommentWriter.cs
index a6c85e9..4ea3297 100644
--- a/src/src/SourceGeneratorShared/XmlCommentWriter.cs
+++ b/src/src/SourceGeneratorShared/XmlCommentWriter.cs
@@ -57,22 +57,32 @@ public CodeWriter XmlCref(string typeName, string content) =>
/// The current writer.
public CodeWriter XmlReturn(params string[] content) => XmlCore(writer, "returns", content);
- /// Writes an XML <value> documentation block.
+ ///
+ /// Writes an XML <value> documentation block.
+ ///
public CodeWriter XmlValue(params string[] content) => XmlCore(writer, "value", content);
- /// Writes an XML <remarks> documentation block.
+ ///
+ /// Writes an XML <remarks> documentation block.
+ ///
public CodeWriter XmlRemarks(params string[] content) => XmlCore(writer, "remarks", content);
- /// Writes an XML <permission> documentation block.
+ ///
+ /// Writes an XML <permission> documentation block.
+ ///
public CodeWriter XmlPermission(string cref, params string[] content) =>
string.IsNullOrWhiteSpace(cref)
? throw new ArgumentException("The XML cref cannot be null or empty.", nameof(cref))
: XmlCore(writer, BuildXmlTag("permission", ("cref", cref)), "permission", content);
- /// Writes a self-closing XML <inheritdoc /> element.
+ ///
+ /// Writes a self-closing XML <inheritdoc /> element.
+ ///
public CodeWriter XmlInheritDoc() => writer.Write("/// ").WriteLine(BuildSelfClosingXmlTag("inheritdoc"));
- /// Writes a self-closing XML <inheritdoc /> element for a member.
+ ///
+ /// Writes a self-closing XML <inheritdoc /> element for a member.
+ ///
public CodeWriter XmlInheritDoc(string cref) =>
string.IsNullOrWhiteSpace(cref)
? throw new ArgumentException("The XML cref cannot be null or empty.", nameof(cref))
@@ -145,7 +155,9 @@ public CodeWriter XmlCode(string content)
return XmlCore(writer, "c", "c", [content], supportsMultiLine: false, compactSingleLine: true);
}
- /// Writes an XML <code> documentation block.
+ ///
+ /// Writes an XML <code> documentation block.
+ ///
public CodeWriter XmlCodeBlock(params string[] content) =>
XmlCore(writer, "code", "code", content, supportsMultiLine: true, compactSingleLine: false);
@@ -156,7 +168,9 @@ public CodeWriter XmlCodeBlock(params string[] content) =>
/// The current writer.
public CodeWriter XmlExample(params string[] content) => XmlCore(writer, "example", content);
- /// Writes an XML <seealso> documentation element.
+ ///
+ /// Writes an XML <seealso> documentation element.
+ ///
public CodeWriter XmlSeeAlso(string cref, params string[] content)
{
if (string.IsNullOrWhiteSpace(cref))
@@ -168,7 +182,9 @@ public CodeWriter XmlSeeAlso(string cref, params string[] content)
: XmlCore(writer, BuildXmlTag("seealso", ("cref", cref)), "seealso", content);
}
- /// Writes an XML <seealso> documentation element for a type.
+ ///
+ /// Writes an XML <seealso> documentation element for a type.
+ ///
public CodeWriter XmlSeeAlso(TypeIdentity type, params string[] content)
{
var cref = ToXmlCref(type);
@@ -178,7 +194,9 @@ public CodeWriter XmlSeeAlso(TypeIdentity type, params string[] content)
: XmlCore(writer, BuildXmlTag("seealso", ("cref", cref)), "seealso", content);
}
- /// Writes an XML <seealso> documentation element for a type reference.
+ ///
+ /// Writes an XML <seealso> documentation element for a type reference.
+ ///
/// If is .
public CodeWriter XmlSeeAlso(TypeReference type, params string[] content)
{
@@ -192,7 +210,9 @@ public CodeWriter XmlSeeAlso(TypeReference type, params string[] content)
: XmlCore(writer, BuildXmlTag("seealso", ("cref", cref)), "seealso", content);
}
- /// Writes a self-closing XML <include /> documentation element.
+ ///
+ /// Writes a self-closing XML <include /> documentation element.
+ ///
public CodeWriter XmlInclude(string file, string path)
{
if (string.IsNullOrWhiteSpace(file))
@@ -284,7 +304,9 @@ public CodeWriter XmlParam(string parameterName, params string[] content)
return XmlCore(writer, startTag, tag, content, compactSingleLine: true);
}
- /// Writes an XML <typeparam> documentation element.
+ ///
+ /// Writes an XML <typeparam> documentation element.
+ ///
public CodeWriter XmlTypeParam(string typeParameterName, params string[] content)
{
if (string.IsNullOrWhiteSpace(typeParameterName))
@@ -427,7 +449,9 @@ public static string BuildXmlTag(string tag, params (string Name, object Value)[
return builder.ToString();
}
- /// Returns an inline XML reference to a type or member.
+ ///
+ /// Returns an inline XML reference to a type or member.
+ ///
public static string XmlSee(string cref, string? description = null)
{
if (string.IsNullOrWhiteSpace(cref))
@@ -439,11 +463,15 @@ public static string XmlSee(string cref, string? description = null)
: BuildXmlTag("see", ("cref", cref)) + description + "";
}
- /// Returns an inline XML reference to a type.
+ ///
+ /// Returns an inline XML reference to a type.
+ ///
public static string XmlSee(TypeIdentity type, string? description = null) =>
XmlSee(ToXmlCref(type), description);
- /// Returns an inline XML reference to a type reference.
+ ///
+ /// Returns an inline XML reference to a type reference.
+ ///
/// If is .
public static string XmlSee(TypeReference type, string? description = null) =>
type == null ? throw new ArgumentNullException(nameof(type)) : XmlSee(ToXmlCref(type), description);
@@ -580,54 +608,84 @@ static void WrapInNullable(StringBuilder builder)
builder.Append("global::System.Nullable{").Append(inner).Append('}');
}
- /// Returns an inline XML <para> element containing the provided content.
+ ///
+ /// Returns an inline XML <para> element containing the provided content.
+ ///
public static string XmlInlinePara(params string[] content) => XmlCore("para", content, false);
- /// Returns an inline XML reference to a parameter.
+ ///
+ /// Returns an inline XML reference to a parameter.
+ ///
public static string XmlParamRef(string parameterName) =>
BuildSelfClosingXmlTag("paramref", ("name", parameterName));
- /// Returns an inline XML reference to a type parameter.
+ ///
+ /// Returns an inline XML reference to a type parameter.
+ ///
public static string XmlTypeParamRef(string typeParameterName) =>
BuildSelfClosingXmlTag("typeparamref", ("name", typeParameterName));
- /// Returns inline code suitable for use in the middle of documentation text.
+ ///
+ /// Returns inline code suitable for use in the middle of documentation text.
+ ///
public static string XmlInlineCode(string content) => $"{content}";
- /// Returns inline code suitable for use in the middle of documentation text.
+ ///
+ /// Returns inline code suitable for use in the middle of documentation text.
+ ///
public static string XmlInlineCodeBlock(params string[] content) => XmlCore("code", content, false);
- /// Escapes plain text for safe composition with XML documentation elements.
+ ///
+ /// Escapes plain text for safe composition with XML documentation elements.
+ ///
public static string XmlText(string content) =>
EscapeXml(content ?? throw new ArgumentNullException(nameof(content)));
- /// Returns an XML line break for use inside documentation text.
+ ///
+ /// Returns an XML line break for use inside documentation text.
+ ///
public static string XmlLineBreak() => "
";
- /// Returns an XML list item containing a description.
+ ///
+ /// Returns an XML list item containing a description.
+ ///
public static string XmlListItem(string description) => $"- {description}
";
- /// Returns an XML list item containing a term and its description.
+ ///
+ /// Returns an XML list item containing a term and its description.
+ ///
public static string XmlListItem(string term, string description) =>
$"- {term}{description}
";
- /// Returns an XML list header containing a term and its description.
+ ///
+ /// Returns an XML list header containing a term and its description.
+ ///
public static string XmlListHeader(string term, string description) =>
$"{term}{description}";
- /// Returns an XML list header containing a term and its description.
+ ///
+ /// Returns an XML list header containing a term and its description.
+ ///
public static string XmlListHeader(string description) => $"{description}";
- /// Returns an XML list term.
+ ///
+ /// Returns an XML list term.
+ ///
public static string XmlTerm(string content) => $"{content}";
- /// Returns an XML list description.
+ ///
+ /// Returns an XML list description.
+ ///
public static string XmlDescription(string content) => $"{content}";
- /// Returns an arbitrary inline XML element.
+ ///
+ /// Returns an arbitrary inline XML element.
+ ///
public static string XmlInlineElement(string tag, string content) => BuildXmlTag(tag) + content + $"{tag}>";
- /// Returns a self-closing XML element with optional attributes.
+ ///
+ /// Returns a self-closing XML element with optional attributes.
+ ///
public static string BuildSelfClosingXmlTag(string tag, params (string Name, object Value)[]? attributes)
{
var openTag = BuildXmlTag(tag, attributes);
diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DiscardedCodeWriterScopeAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DiscardedCodeWriterScopeAnalyzerTests.cs
new file mode 100644
index 0000000..b194d58
--- /dev/null
+++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DiscardedCodeWriterScopeAnalyzerTests.cs
@@ -0,0 +1,120 @@
+using Purview.SourceGeneratorFramework.Testing;
+using Purview.SourceGeneratorFramework.Testing.TUnit;
+
+namespace Purview.SourceGeneratorFramework.Analyzers;
+
+public sealed class DiscardedCodeWriterScopeAnalyzerTests
+ : TUnitDiagnosticAnalyzerTestBase
+{
+ [Test]
+ public async Task DiscardedScope_ReportsDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ class Emitter
+ {
+ public void Emit()
+ {
+ var writer = new CodeWriter(new GenerationSettings("G"));
+ writer.WriteClassScope(new TypeDeclarationOptions("C"));
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions
+ {
+ AdditionalAssemblyTypes =
+ [
+ typeof(CodeWriter),
+ typeof(GenerationSettings),
+ typeof(TypeDeclarationOptions),
+ ],
+ },
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(result).HasDiagnostics(1);
+ await Assert.That(result).HasDiagnostic(DiscardedCodeWriterScopeAnalyzer.Rule.Id);
+ }
+
+ [Test]
+ public async Task ScopeUsedInUsing_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ class Emitter
+ {
+ public void Emit()
+ {
+ var writer = new CodeWriter(new GenerationSettings("G"));
+ using (writer.WriteClassScope(new TypeDeclarationOptions("C")))
+ {
+ writer.WriteLine("// body");
+ }
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions
+ {
+ AdditionalAssemblyTypes =
+ [
+ typeof(CodeWriter),
+ typeof(GenerationSettings),
+ typeof(TypeDeclarationOptions),
+ ],
+ },
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task NonScopeMethod_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ class Emitter
+ {
+ public void Emit()
+ {
+ var writer = new CodeWriter(new GenerationSettings("G"));
+ writer.WriteProperty(new PropertyDeclarationOptions("Name", TypeIdentity.Create()));
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions
+ {
+ AdditionalAssemblyTypes =
+ [
+ typeof(CodeWriter),
+ typeof(GenerationSettings),
+ typeof(PropertyDeclarationOptions),
+ ],
+ },
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(result).HasNoDiagnostics();
+ }
+}
diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterApiAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterApiAnalyzerTests.cs
new file mode 100644
index 0000000..e4c7431
--- /dev/null
+++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterApiAnalyzerTests.cs
@@ -0,0 +1,121 @@
+using Purview.SourceGeneratorFramework.Testing;
+using Purview.SourceGeneratorFramework.Testing.TUnit;
+
+namespace Purview.SourceGeneratorFramework.Analyzers;
+
+public sealed class PreferStructuredCodeWriterApiAnalyzerTests
+ : TUnitDiagnosticAnalyzerTestBase
+{
+ [Test]
+ public async Task WriteLine_WithClassDeclaration_ReportsDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ class Emitter
+ {
+ public void Emit()
+ {
+ var writer = new CodeWriter(new GenerationSettings("G"));
+ writer.WriteLine("public class C { }");
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] },
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(result).HasDiagnostics(1);
+ await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterApiAnalyzer.Rule.Id);
+ }
+
+ [Test]
+ public async Task WriteLine_WithStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ class Emitter
+ {
+ public void Emit()
+ {
+ var writer = new CodeWriter(new GenerationSettings("G"));
+ writer.WriteLine("return value;");
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] },
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task WriteLine_WithUsingStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ class Emitter
+ {
+ public void Emit()
+ {
+ var writer = new CodeWriter(new GenerationSettings("G"));
+ writer.WriteLine("using (var stream = Open())");
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] },
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task WriteMethodCall_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ class Emitter
+ {
+ public void Emit()
+ {
+ var writer = new CodeWriter(new GenerationSettings("G"));
+ writer.WriteMethodCall("Run", "value");
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] },
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(result).HasNoDiagnostics();
+ }
+}
diff --git a/src/tests/SourceGeneratorFramework.Generators.UnitTests/AttributeDataModelGeneratorTests.cs b/src/tests/SourceGeneratorFramework.Generators.UnitTests/AttributeDataModelGeneratorTests.cs
index ac41023..a2517ad 100644
--- a/src/tests/SourceGeneratorFramework.Generators.UnitTests/AttributeDataModelGeneratorTests.cs
+++ b/src/tests/SourceGeneratorFramework.Generators.UnitTests/AttributeDataModelGeneratorTests.cs
@@ -59,7 +59,7 @@ await Assert
"global::System.Collections.Generic.IEnumerable<(RequiredAttributeData Instance, global::Microsoft.CodeAnalysis.AttributeData Attribute)> AllAttributeData("
);
await Assert.That(generated).Contains("yield return (instance, attributes[i]);");
- await Assert.That(generated).Contains("global::Microsoft.CodeAnalysis.ISymbol symbol)");
+ await Assert.That(generated).Contains("global::Microsoft.CodeAnalysis.ISymbol symbol\n\t\t)");
await Assert.That(generated).Contains("return AllAttributeData(symbol.GetAttributes());");
await Assert.That(generated).Contains("return FromAttributeData(symbol.GetAttributes());");
await Assert.That(generated).Contains("return FromAttributeData(symbol.GetAttributes(), out attribute);");
diff --git a/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs b/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs
index 74b6af8..cec3bd4 100644
--- a/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs
+++ b/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs
@@ -204,4 +204,132 @@ await Assert
.That(() => query.Get(static method => method.Identifier.ValueText == "Nope"))
.Throws();
}
+
+ [Test]
+ public async Task NestedQuery_GivenRoot_ScopesNodeSearches()
+ {
+ var query = CreateQuery();
+ var method = query.GetMethod("DoWork");
+
+ var nested = query.In(method);
+
+ await Assert.That(nested.Has()).IsTrue();
+ await Assert.That(nested.HasMethod("Compute")).IsFalse();
+ await Assert.That(nested.HasProperty("Count")).IsFalse();
+ }
+
+ [Test]
+ public async Task NestedQuery_ChainingExtension_ScopesSearches()
+ {
+ var query = CreateQuery();
+
+ var nested = query.GetClass("Sample").Query(query);
+
+ await Assert.That(nested.HasProperty("Count")).IsTrue();
+ await Assert.That(nested.HasField("Constant")).IsTrue();
+ await Assert.That(nested.HasMethod("DoWork")).IsTrue();
+ await Assert.That(nested.HasClass("Sample")).IsFalse();
+ }
+
+ [Test]
+ public async Task GetAllAndCount_ReturnAllMatches()
+ {
+ var query = CreateQuery();
+
+ await Assert.That(query.Count()).IsEqualTo(2);
+ await Assert.That(query.GetAll().Length).IsEqualTo(3);
+ await Assert
+ .That(
+ query
+ .GetAll(static method => method.Identifier.ValueText.StartsWith('C'))
+ .Length
+ )
+ .IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task OperatorAndIndexerAndAttributeQueries_FindDeclarations()
+ {
+ // Arrange
+ const string source = """
+ using System;
+
+ namespace Test;
+
+ public struct Money
+ {
+ [Obsolete]
+ public string this[int index] => "";
+
+ public static bool operator ==(Money left, Money right) => true;
+ public static bool operator !=(Money left, Money right) => false;
+ public static implicit operator int(Money value) => 0;
+ }
+ """;
+ var (compilation, _) = TestCompilation.CreateWithRoot(source);
+ var query = new CodeQuery([.. compilation.SyntaxTrees], compilation);
+
+ // Act / Assert
+ await Assert.That(query.HasOperator("==")).IsTrue();
+ await Assert.That(query.HasOperator("!=")).IsTrue();
+ await Assert.That(query.HasConversionOperator("implicit")).IsTrue();
+ await Assert.That(query.HasOperator("+")).IsFalse();
+ await Assert.That(query.GetOperator("==").ParameterList.Parameters.Count).IsEqualTo(2);
+
+ await Assert.That(query.HasIndexer(TypeReference.Create())).IsTrue();
+ await Assert.That(query.HasIndexer(TypeReference.Create())).IsFalse();
+
+ var money = query.GetStruct("Money");
+ await Assert.That(query.HasAttribute(money, "Obsolete")).IsTrue();
+ await Assert.That(query.GetAttribute(money, "Obsolete")).IsNotNull();
+ await Assert.That(query.HasAttribute(money, "Missing")).IsFalse();
+ }
+
+ [Test]
+ public async Task StatementQueries_FindStatementsAndInvocations()
+ {
+ // Arrange
+ const string source = """
+ using System;
+ using System.Collections.Generic;
+
+ namespace Test;
+
+ public class Worker
+ {
+ public void Run(List items)
+ {
+ var total = 0;
+ try
+ {
+ foreach (var item in items)
+ {
+ total += int.Parse(item);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+ if (total > 0)
+ {
+ Process(total);
+ }
+ }
+
+ void Process(int value) { }
+ }
+ """;
+ var (compilation, _) = TestCompilation.CreateWithRoot(source);
+ var query = new CodeQuery([.. compilation.SyntaxTrees], compilation);
+
+ // Act / Assert
+ await Assert.That(query.HasTry()).IsTrue();
+ await Assert.That(query.HasForeach()).IsTrue();
+ await Assert.That(query.HasIf()).IsTrue();
+ await Assert.That(query.HasWhile("true")).IsFalse();
+ await Assert.That(query.HasInvocation("Parse")).IsTrue();
+ await Assert.That(query.HasInvocation("Process")).IsTrue();
+ await Assert.That(query.HasInvocation("Missing")).IsFalse();
+ }
}
diff --git a/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs b/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs
index 6adb31c..1d90a12 100644
--- a/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs
+++ b/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs
@@ -1,3 +1,5 @@
+using Microsoft.CodeAnalysis.CSharp;
+
namespace Purview.SourceGeneratorFramework;
public class CodeWriterTests
@@ -940,7 +942,8 @@ await Assert
+ "public global::Aspire.Hosting.IDistributedApplicationBuilder AddAspireResourceKit(\n"
+ "\tglobal::System.Action? onBuilt = null,\n"
+ "\tglobal::System.Action? onConfigured = null,\n"
- + "\tglobal::System.Action>? configureOptions = null)\n"
+ + "\tglobal::System.Action>? configureOptions = null\n"
+ + ")\n"
+ "{\n"
+ "\treturn builder;\n"
+ "}\n"
@@ -1222,7 +1225,8 @@ await Assert
.IsEqualTo(
GeneratedAttributes()
+ "global::System.Collections.Generic.Dictionary[]? Load(\n"
- + "\tglobal::System.Collections.Generic.List? items) => items.ToArray();\n"
+ + "\tglobal::System.Collections.Generic.List? items\n"
+ + ") => items.ToArray();\n"
);
}
@@ -1624,7 +1628,7 @@ public async Task WriteAwaitedMethodCall_WithStructuredArguments_WritesReceiverA
writeArgumentsOnSeparateLines: true
);
- await Assert.That(writer.ToString()).IsEqualTo("await service.LoadAsync(\n\ttoken,\n\tout result);\n");
+ await Assert.That(writer.ToString()).IsEqualTo("await service.LoadAsync(\n\ttoken,\n\tout result\n);\n");
}
[Test]
@@ -1644,7 +1648,8 @@ await Assert
.IsEqualTo(
"factory.Create(\n"
+ "\tfirstArgumentWithANameThatMakesTheCallLong,\n"
- + "\tsecondArgumentWithANameThatMakesTheCallLong);\n"
+ + "\tsecondArgumentWithANameThatMakesTheCallLong\n"
+ + ");\n"
);
}
@@ -1665,7 +1670,7 @@ public async Task WriteMethodCall_WithStructuredArguments_WritesModifiersAndMult
await Assert
.That(writer.ToString())
- .IsEqualTo("AMethodCallWithLotsOfParams(\n" + "\tref a-long-a-param,\n" + "\tout another-long-param);\n");
+ .IsEqualTo("AMethodCallWithLotsOfParams(\n" + "\tref a-long-a-param,\n" + "\tout another-long-param\n);\n");
}
[Test]
@@ -1699,10 +1704,12 @@ await Assert
.IsEqualTo(
"var @event = new ASpecificType(\n"
+ "\tpropVal1,\n"
- + "\tsecond: propVal2);\n"
+ + "\tsecond: propVal2\n"
+ + ");\n"
+ "existingEvent = new ASpecificType(\n"
+ "\tpropVal1,\n"
- + "\tsecond: propVal2);\n"
+ + "\tsecond: propVal2\n"
+ + ");\n"
);
}
@@ -2468,4 +2475,894 @@ partial void Apply()
"""
);
}
+
+ [Test]
+ public async Task ModernizationOptions_AreValueTypes()
+ {
+ // Arrange / Act / Assert
+ await Assert.That(typeof(OperatorDeclarationOptions).IsValueType).IsTrue();
+ await Assert.That(typeof(ObjectInitializerMemberOptions).IsValueType).IsTrue();
+ }
+
+ [Test]
+ public async Task WriteOperator_GivenBlockBody_WritesAccessibilityStaticTokenAndParameters()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "==",
+ Type("bool"),
+ new("left", Type("global::Testing.Name")),
+ new("right", Type("global::Testing.Name"))
+ )
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ };
+
+ // Act
+ writer.WriteOperator(declaration, body => body.WriteLine("return left.Equals(right);"));
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ GeneratedAttributes()
+ + "public static bool operator ==(global::Testing.Name left, global::Testing.Name right)\n"
+ + "{\n"
+ + "\treturn left.Equals(right);\n"
+ + "}\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteOperator_GivenExpressionBody_WritesExpressionAndBalancesScope()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "<",
+ Type("bool"),
+ new("left", Type("global::Testing.Money")),
+ new("right", Type("global::Testing.Money"))
+ )
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ ExpressionBody = "left.CompareTo(right) < 0",
+ };
+
+ // Act
+ using (writer.WriteOperatorScope(declaration))
+ {
+ // Intentionally empty: an expression-bodied operator returns an empty scope.
+ }
+
+ // Assert
+ await Assert.That(writer.OpenScopeCount).IsEqualTo(0);
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ GeneratedAttributes()
+ + "public static bool operator <(global::Testing.Money left, global::Testing.Money right) => left.CompareTo(right) < 0;\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteOperatorScope_GivenBlockBody_TracksOpenScopeCount()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "==",
+ Type("bool"),
+ new("left", Type("Name")),
+ new("right", Type("Name"))
+ );
+
+ // Act
+ using (writer.WriteOperatorScope(declaration))
+ {
+ writer.WriteLine("return left.Equals(right);");
+ await Assert.That(writer.OpenScopeCount).IsEqualTo(1);
+ }
+
+ // Assert
+ await Assert.That(writer.OpenScopeCount).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task WriteOperator_GivenNullBody_Throws()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "==",
+ Type("bool"),
+ new("left", Type("Name")),
+ new("right", Type("Name"))
+ );
+
+ // Act / Assert
+ await Assert.That(() => writer.WriteOperator(declaration, null!)).Throws();
+ }
+
+ [Test]
+ public async Task WriteOperator_GivenExpressionBodyAndCallback_ThrowsWithoutWriting()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "==",
+ Type("bool"),
+ new("left", Type("Name")),
+ new("right", Type("Name"))
+ )
+ {
+ ExpressionBody = "left.Equals(right)",
+ };
+
+ // Act / Assert
+ await Assert.That(() => writer.WriteOperator(declaration, _ => { })).Throws();
+ await Assert.That(writer.ToString()).IsEmpty();
+ }
+
+ [Test]
+ public async Task WritePartialMethod_GivenIsReadOnly_WritesReadonlyModifier()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new MethodDeclarationOptions("OnValidate", Type("void"))
+ {
+ IsPartial = true,
+ IsReadOnly = true,
+ Parameters =
+ [
+ new("id", Type("global::System.Guid")),
+ new("displayName", Type("string").Nullable()),
+ new("isActive", Type("bool")),
+ ],
+ };
+
+ // Act
+ writer.WritePartialMethod(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ GeneratedAttributes()
+ + "readonly partial void OnValidate(global::System.Guid id, string? displayName, bool isActive);\n"
+ );
+ }
+
+ [Test]
+ public async Task WritePartialMethod_GivenIsReadOnlyFalse_OmitsReadonlyModifier()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new MethodDeclarationOptions("Apply", Type("void")) { IsPartial = true };
+
+ // Act
+ writer.WritePartialMethod(declaration);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "partial void Apply();\n");
+ }
+
+ [Test]
+ public async Task WriteMethod_GivenIsReadOnlyAndIsStatic_ThrowsWithoutWriting()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new MethodDeclarationOptions("Invalid", Type("void")) { IsReadOnly = true, IsStatic = true };
+
+ // Act / Assert
+ await Assert.That(() => writer.WriteMethodScope(declaration)).Throws();
+ await Assert.That(writer.ToString()).IsEmpty();
+ }
+
+ [Test]
+ public async Task WriteAssignment_WithObjectInitializerMembers_WritesBlockInitializer()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var creation = new ObjectCreationOptions(Type("global::Testing.OrderAggregate"))
+ {
+ InitializerMembers =
+ [
+ new("Details", "jsonModel.Details ?? new global::Purview.EventSourcing.Aggregates.AggregateDetails()"),
+ new("CustomerId", "jsonModel.CustomerId"),
+ ],
+ };
+
+ // Act
+ writer.WriteAssignment("var", "aggregate", creation);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ "var aggregate = new global::Testing.OrderAggregate\n"
+ + "{\n"
+ + "\tDetails = jsonModel.Details ?? new global::Purview.EventSourcing.Aggregates.AggregateDetails(),\n"
+ + "\tCustomerId = jsonModel.CustomerId,\n"
+ + "};\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteAssignment_WithObjectInitializerMembersAndConstructorArguments_WritesArgumentsBeforeInitializer()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var creation = new ObjectCreationOptions(
+ Type("global::Testing.OrderEvents.OrderCreatedEvent"),
+ "customerId",
+ "total"
+ )
+ {
+ InitializerMembers = [new("CustomerId", "customerId"), new("Total", "total")],
+ };
+
+ // Act
+ writer.WriteAssignment("var", "@event", creation);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ "var @event = new global::Testing.OrderEvents.OrderCreatedEvent(customerId, total)\n"
+ + "{\n"
+ + "\tCustomerId = customerId,\n"
+ + "\tTotal = total,\n"
+ + "};\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteAssignment_WithInlineInitializerMembers_WritesSingleLineInitializer()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var creation = new ObjectCreationOptions(Type("Order"))
+ {
+ InitializerMembers = [new("A", "1"), new("B", "2")],
+ WriteInitializerMembersOnSeparateLines = false,
+ };
+
+ // Act
+ writer.WriteAssignment("var order", creation);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("var order = new Order { A = 1, B = 2, };\n");
+ }
+
+ [Test]
+ public async Task WriteAssignment_WithEmptyInitializerMembers_RendersAsToday()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var creation = new ObjectCreationOptions(Type("Order"));
+
+ // Act
+ writer.WriteAssignment("var order", creation);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("var order = new Order();\n");
+ }
+
+ [Test]
+ public async Task WriteAssignment_WithEmptyArgumentsAndForceNotNull_WritesBangAfterParentheses()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var creation = new ObjectCreationOptions(Type("Order"));
+
+ // Act
+ writer.WriteAssignment("var order", creation, forceNotNull: true);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("var order = new Order()!;\n");
+ }
+
+ [Test]
+ public async Task WriteAssignment_WithInitializerMembersAndForceNotNull_WritesBangAfterBrace()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var creation = new ObjectCreationOptions(Type("Order"))
+ {
+ InitializerMembers = [new("A", "1")],
+ WriteInitializerMembersOnSeparateLines = false,
+ };
+
+ // Act
+ writer.WriteAssignment("var order", creation, forceNotNull: true);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("var order = new Order { A = 1, }!;\n");
+ }
+
+ [Test]
+ public async Task WriteAssignment_WithMultilineArgumentsAndInitializerMembers_WritesClosingBraceAndSemicolon()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var creation = new ObjectCreationOptions(Type("Order"), "aParameterNameThatForcesTheArgumentsOntoTheirOwnLine")
+ {
+ WriteArgumentsOnSeparateLines = true,
+ InitializerMembers = [new("A", "1")],
+ };
+
+ // Act
+ writer.WriteAssignment("var order", creation);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ "var order = new Order(\n"
+ + "\taParameterNameThatForcesTheArgumentsOntoTheirOwnLine\n"
+ + ")\n"
+ + "{\n"
+ + "\tA = 1,\n"
+ + "};\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteThrow_GivenExceptionTypeAndMessage_WritesThrow()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteThrow(
+ new TypeIdentity("InvalidOperationException", "System"),
+ "Collection property 'Tags' cannot be null."
+ );
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ "throw new global::System.InvalidOperationException(\"Collection property 'Tags' cannot be null.\");\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteThrow_GivenMessageWithQuotesAndBackslashes_EscapesMessage()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteThrow(new TypeIdentity("InvalidOperationException", "System"), "He said \"hi\" to C:\\temp\\file.");
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ "throw new global::System.InvalidOperationException(\"He said \\\"hi\\\" to C:\\\\temp\\\\file.\");\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteThrow_GivenNullMessage_WritesEmptyConstructor()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteThrow(new TypeIdentity("InvalidOperationException", "System"));
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("throw new global::System.InvalidOperationException();\n");
+ }
+
+ [Test]
+ public async Task WriteOperator_GivenImplicitConversion_WritesConversionOperator()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "implicit",
+ Type("global::Testing.Widget"),
+ new("source", Type("global::Testing.RawWidget"))
+ )
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ Kind = OperatorDeclarationKind.ImplicitConversion,
+ ExpressionBody = "new global::Testing.Widget(source.Value)",
+ };
+
+ // Act
+ writer.WriteOperatorScope(declaration);
+
+ // Assert
+ await Assert.That(writer.OpenScopeCount).IsEqualTo(0);
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ GeneratedAttributes()
+ + "public static implicit operator global::Testing.Widget(global::Testing.RawWidget source) => new global::Testing.Widget(source.Value);\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteOperator_GivenExplicitConversion_WritesExplicitKeyword()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "explicit",
+ Type("global::Testing.RawWidget"),
+ new("widget", Type("global::Testing.Widget"))
+ )
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ Kind = OperatorDeclarationKind.ExplicitConversion,
+ };
+
+ // Act
+ writer.WriteOperator(
+ declaration,
+ body => body.WriteLine("return new global::Testing.RawWidget(widget.Value);")
+ );
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ GeneratedAttributes()
+ + "public static explicit operator global::Testing.RawWidget(global::Testing.Widget widget)\n"
+ + "{\n"
+ + "\treturn new global::Testing.RawWidget(widget.Value);\n"
+ + "}\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteOperator_GivenUnaryOperator_WritesSingleOperand()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new OperatorDeclarationOptions(
+ "-",
+ Type("global::Testing.Money"),
+ new("value", Type("global::Testing.Money"))
+ )
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ Kind = OperatorDeclarationKind.Unary,
+ };
+
+ // Act
+ writer.WriteOperator(declaration, body => body.WriteLine("return new global::Testing.Money(-value.Amount);"));
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ GeneratedAttributes()
+ + "public static global::Testing.Money operator -(global::Testing.Money value)\n"
+ + "{\n"
+ + "\treturn new global::Testing.Money(-value.Amount);\n"
+ + "}\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteProperty_GivenRequired_WritesRequiredModifier()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new PropertyDeclarationOptions("Name", Type("string"))
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ IsRequired = true,
+ IsInitOnly = true,
+ };
+
+ // Act
+ writer.WriteProperty(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(GeneratedAttributes() + "public required string Name { get; init; }\n");
+ }
+
+ [Test]
+ public async Task WriteField_GivenRequired_WritesRequiredModifier()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new FieldDeclarationOptions("_name", Type("string"))
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ IsRequired = true,
+ };
+
+ // Act
+ writer.WriteField(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(GeneratedAttributes(includeCoverageExclusion: false) + "public required string _name;\n");
+ }
+
+ [Test]
+ public async Task WriteIndexer_GivenAutoAccessors_WritesIndexer()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new IndexerDeclarationOptions(Type("string"), [new("index", Type("int"))])
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ HasSetter = true,
+ };
+
+ // Act
+ writer.WriteIndexer(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(GeneratedAttributes() + "public string this[int index] { get; set; }\n");
+ }
+
+ [Test]
+ public async Task WriteIndexer_GivenExpressionBody_WritesExpressionIndexer()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new IndexerDeclarationOptions(Type("string"), [new("index", Type("int"))])
+ {
+ ExpressionBody = "_items[index]",
+ };
+
+ // Act
+ writer.WriteIndexer(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(GeneratedAttributes() + "string this[int index] => _items[index];\n");
+ }
+
+ [Test]
+ public async Task WriteIndexer_GivenAccessorBodies_WritesScopedAccessors()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new IndexerDeclarationOptions(Type("string"), [new("index", Type("int"))])
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ HasSetter = true,
+ };
+
+ // Act
+ writer.WriteIndexer(
+ declaration,
+ getter => getter.WriteLine("return _items[index];"),
+ setter => setter.WriteLine("_items[index] = value;")
+ );
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(
+ GeneratedAttributes()
+ + "public string this[int index]\n"
+ + "{\n"
+ + "\tget\n\t{\n\t\treturn _items[index];\n\t}\n"
+ + "\tset\n\t{\n\t\t_items[index] = value;\n\t}\n"
+ + "}\n"
+ );
+ }
+
+ [Test]
+ public async Task WriteStatementFamily_WritesStructuredBlocks()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteTry(tryBody =>
+ tryBody.WriteForeach(
+ "var item in items",
+ foreachBody =>
+ foreachBody.WriteIfElse(
+ "item is null",
+ ifBody => ifBody.WriteThrow(TypeIdentity.Create(), "Null item"),
+ elseBody => elseBody.WriteMethodCall("Process", "item")
+ )
+ )
+ );
+ writer.WriteCatch(TypeIdentity.Create(), "ex", catchBody => catchBody.WriteMethodCall("Log", "ex"));
+ writer.WriteFinally(finallyBody => finallyBody.WriteMethodCall("Dispose"));
+ writer.WriteWhile("!finished", whileBody => whileBody.WriteMethodCall("Advance"));
+ writer.WriteUsingStatement("var stream = Open()", usingBody => usingBody.WriteMethodCall("Read", "stream"));
+ writer.WriteLockStatement("_gate", lockBody => lockBody.WriteMethodCall("Run"));
+
+ // Assert
+ var result = writer.ToString();
+ await Assert
+ .That(result)
+ .Contains(
+ "try\n{\n\tforeach (var item in items)\n\t{\n\t\tif (item is null)\n\t\t{\n\t\t\tthrow new global::System.InvalidOperationException(\"Null item\");\n\t\t}\n\t\telse\n\t\t{\n\t\t\tProcess(item);\n\t\t}\n\t}"
+ );
+ await Assert.That(result).Contains("catch (global::System.Exception ex)\n{\n\tLog(ex);\n}");
+ await Assert.That(result).Contains("finally\n{\n\tDispose();\n}");
+ await Assert.That(result).Contains("while (!finished)\n{\n\tAdvance();\n}");
+ await Assert.That(result).Contains("using (var stream = Open())\n{\n\tRead(stream);\n}");
+ await Assert.That(result).Contains("lock (_gate)\n{\n\tRun();\n}");
+ await Assert.That(writer.OpenScopeCount).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task WriteDoWhile_GivenCondition_WritesTrailingCondition()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteDoWhile("!finished", body => body.WriteMethodCall("Advance"));
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("do\n{\n\tAdvance();\n} while (!finished);\n");
+ }
+
+ [Test]
+ public async Task OpenRegion_GivenName_WritesRegionDirectives()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.OpenRegion("Generated members", body => body.WriteLine("public int Value { get; }"));
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo("#region Generated members\n\tpublic int Value { get; }\n#endregion\n");
+ }
+
+ [Test]
+ public async Task WriteUsing_GivenGlobal_WritesGlobalUsing()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteUsing("System.Linq", isGlobal: true);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("global using System.Linq;\n");
+ }
+
+ [Test]
+ public async Task WriteUsingAlias_WritesAliasDirective()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteUsingAlias("Events", "global::Purview.Events");
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("using Events = global::Purview.Events;\n");
+ }
+
+ [Test]
+ public async Task WriteMethod_GivenSpacesIndentation_UsesConfiguredSize()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests(
+ settings: new GenerationSettings("TestGenerator", "1.0.0")
+ {
+ IndentationStyle = IndentationStyle.Spaces,
+ IndentationSize = 2,
+ }
+ );
+
+ // Act
+ using (
+ writer.WriteMethodScope(
+ new MethodDeclarationOptions("M", Type("void")) { Accessibility = TypeDeclarationAccessibility.Public }
+ )
+ )
+ {
+ writer.WriteLine("return;");
+ }
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public void M()\n{\n return;\n}\n");
+ }
+
+ [Test]
+ public async Task RenderFullName_GivenTypeParameterNullableAndDisabledContext_ElidesAnnotation()
+ {
+ // Arrange
+ var typeParameter = TypeReference.ForTypeParameter("T").Nullable();
+ var dynamic = TypeReference.Dynamic.Nullable();
+
+ // Act / Assert
+ await Assert.That(typeParameter.RenderFullNameForNullable(nullableSupported: false)).IsEqualTo("T");
+ await Assert.That(dynamic.RenderFullNameForNullable(nullableSupported: false)).IsEqualTo("dynamic");
+ }
+
+ [Test]
+ public async Task RenderFullName_GivenTypeParameterNullableAndEnabledContext_KeepsAnnotation()
+ {
+ // Arrange
+ var typeParameter = TypeReference.ForTypeParameter("T").Nullable();
+
+ // Act / Assert
+ await Assert.That(typeParameter.RenderFullNameForNullable(nullableSupported: true)).IsEqualTo("T?");
+ }
+
+ [Test]
+ public async Task GenerationSettings_GivenLanguageVersion_StoresIt()
+ {
+ // Arrange / Act
+ var settings = new GenerationSettings("G") { LanguageVersion = LanguageVersion.CSharp12 };
+
+ // Assert
+ await Assert.That(settings.LanguageVersion).IsEqualTo(LanguageVersion.CSharp12);
+ }
+
+ [Test]
+ public async Task WriteProperty_GivenIsFieldBacked_WritesFieldKeywordAccessors()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new PropertyDeclarationOptions("Value", Type("int"))
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ HasSetter = true,
+ IsFieldBacked = true,
+ };
+
+ // Act
+ writer.WriteProperty(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(GeneratedAttributes() + "public int Value { get => field; set => field = value; }\n");
+ }
+
+ [Test]
+ public async Task WriteProperty_GivenIsFieldBackedInitOnly_WritesFieldKeywordInit()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new PropertyDeclarationOptions("Name", Type("string"))
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ IsInitOnly = true,
+ IsFieldBacked = true,
+ };
+
+ // Act
+ writer.WriteProperty(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(GeneratedAttributes() + "public string Name { get => field; init => field = value; }\n");
+ }
+
+ [Test]
+ public async Task WriteProperty_GivenIsFieldBackedAndExpressionBody_ThrowsWithoutWriting()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new PropertyDeclarationOptions("Value", Type("int"))
+ {
+ IsFieldBacked = true,
+ ExpressionBody = "field",
+ };
+
+ // Act / Assert
+ await Assert.That(() => writer.WriteProperty(declaration)).Throws();
+ await Assert.That(writer.ToString()).IsEmpty();
+ }
+
+ [Test]
+ public async Task WriteStruct_GivenIsRefStruct_WritesRefStruct()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new TypeDeclarationOptions("Buffer")
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ Kind = TypeDeclarationKind.Struct,
+ IsRefStruct = true,
+ IsPartial = false,
+ };
+
+ // Act
+ using (writer.WriteStructScope(declaration))
+ {
+ // Intentionally empty.
+ }
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public ref struct Buffer\n{\n}\n");
+ }
+
+ [Test]
+ public async Task WriteStruct_GivenIsRefStructOnRecordStruct_Throws()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new TypeDeclarationOptions("Invalid")
+ {
+ Kind = TypeDeclarationKind.RecordStruct,
+ IsRefStruct = true,
+ };
+
+ // Act / Assert
+ await Assert.That(() => writer.WriteRecordStructScope(declaration)).Throws();
+ }
+
+ [Test]
+ public async Task WriteField_GivenIsRefField_WritesRefField()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new FieldDeclarationOptions("_value", Type("int"))
+ {
+ Accessibility = TypeDeclarationAccessibility.Public,
+ IsRefField = true,
+ };
+
+ // Act
+ writer.WriteField(declaration);
+
+ // Assert
+ await Assert
+ .That(writer.ToString())
+ .IsEqualTo(GeneratedAttributes(includeCoverageExclusion: false) + "public ref int _value;\n");
+ }
+
+ [Test]
+ public async Task WriteField_GivenIsRefFieldAndInitializer_Throws()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+ var declaration = new FieldDeclarationOptions("_value", Type("int")) { IsRefField = true, Initializer = "0" };
+
+ // Act / Assert
+ await Assert.That(() => writer.WriteField(declaration)).Throws();
+ }
+
+ [Test]
+ public async Task WriteCollectionExpression_GivenItems_WritesInlineExpression()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteCollectionExpression(["first", "second", "..rest"]);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("[first, second, ..rest]");
+ }
+
+ [Test]
+ public async Task WriteCollectionExpression_GivenSeparateLines_WritesMultilineExpression()
+ {
+ // Arrange
+ var writer = CodeWriterFactory.ForTests();
+
+ // Act
+ writer.WriteCollectionExpression(["first", "second"], writeOnSeparateLines: true);
+
+ // Assert
+ await Assert.That(writer.ToString()).IsEqualTo("[\n\tfirst,\n\tsecond\n]");
+ }
}