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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ a diagnostic analyzer, a code fix, tests, samples, and documentation for scalar
- Generated output must be stable for identical input.
- Diagnostics are public developer experience: preserve IDs and meanings, choose accurate locations and severity,
and update `AnalyzerReleases.*.md` for newly introduced or changed diagnostics.
- Cross-component contracts must go through **public** members. The artifact that consumers (and Visual Studio)
load is the merged, self-contained assembly the `Purview.SourceGeneratorFramework` merge pass produces, and
that pass strips every `InternalsVisibleTo` declaration. A component that reads another component's internals
(for example a code fix reading a diagnostic descriptor) therefore compiles against the unmerged build output
and throws `FieldAccessException` at runtime in the IDE. Do not add `InternalsVisibleTo` between Roslyn
components; expose the shared identity publicly (see `DiagnosticLibrary`).
- Source-generator changes normally require tests in `src/tests/SourceGenerator.UnitTests`.

## Runtime project rules
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ dotnet add package Purview.ValueObjects
```

The package ships the runtime contracts (`[Scalar]`, `[ValueObject]`, `IValueObject`, ...), the source generator,
and the diagnostic analyzer. There is no dependency on any event-sourcing library.
the diagnostic analyzer, and code fixes for the analyzer's diagnostics (for example `VO1001` offers
**Add 'partial' modifier**). There is no dependency on any event-sourcing library.

## Quick start

Expand Down Expand Up @@ -185,6 +186,9 @@ Set `DisableValueObjectsSourceGenerator` to `true` in your project:
- `src/src/EFDomainSample.Domain` / `src/src/EFDomainSample.Persistence` – a domain project without Entity
Framework and the persistence project that maps its value objects.
- `src/src/SourceGenerator` – incremental source generator + analyzer.
- `src/src/SourceGenerator.Refactorings` – code fix for the "must be partial" diagnostic.
- `src/src/SourceGenerator.Refactorings` – code fixes for the generator's diagnostics (the "must be partial"
fix). It consumes the generator's diagnostic identities through **public** members only: the shipped analyzer
is the merged, self-contained artifact the `Purview.SourceGeneratorFramework` merge pass produces, and that
pass strips every `InternalsVisibleTo` declaration.
- `src/tests` – unit and source-generator tests.
- `docs` – design and usage guidance.
3 changes: 2 additions & 1 deletion docs/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ This guide walks through modeling DTOs and domain values with `Purview.ValueObje
dotnet add package Purview.ValueObjects
```

The package includes the runtime contracts, the source generator, and the diagnostic analyzer.
The package includes the runtime contracts, the source generator, the diagnostic analyzer, and code fixes for
the analyzer's diagnostics (for example `VO1001` offers **Add 'partial' modifier**).

## 2. Scalar value objects

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purview-value-objects",
"version": "1.0.0-prerelease.10",
"version": "1.0.0-prerelease.11",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
</ItemGroup>

<ItemGroup>
<!-- Requires the source-generator/analyzer assembly for its shared diagnostic descriptors. -->
<!-- Requires the source-generator/analyzer assembly for its shared diagnostic identifiers. Those
identifiers must be public: the shipped analyzer is a merged, self-contained artifact whose
InternalsVisibleTo declarations are stripped, so internals would throw FieldAccessException
at runtime in the IDE. -->
<ProjectReference Include="..\SourceGenerator\SourceGenerator.csproj" />
</ItemGroup>
</Project>
15 changes: 14 additions & 1 deletion src/src/SourceGenerator/Common/DiagnosticLibrary.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
namespace Purview.ValueObjects.SourceGenerator.Common;

static class DiagnosticLibrary
/// <summary>
/// The diagnostics this component produces.
/// <para>
/// The type is public because the identities are shared with the code-fix component
/// (<c>Purview.ValueObjects.SourceGenerator.Refactorings</c>). Consumers receive the merged,
/// self-contained analyzer that the Purview.SourceGeneratorFramework merge pass produces, and that
/// pass strips every <c>InternalsVisibleTo</c> declaration from the artifact. Reaching these members
/// through internals therefore compiles against the unmerged build output and then fails with a
/// <see cref="System.FieldAccessException"/> in the IDE, as soon as Roslyn reads a fixable
/// diagnostic id from the shipped analyzer. Public members are the only cross-component contract a
/// merged artifact preserves.
/// </para>
/// </summary>
public static class DiagnosticLibrary
{
const string ValueObjectCategory = "ValueObjects";

Expand Down
10 changes: 5 additions & 5 deletions src/src/SourceGenerator/SourceGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
<Using Include="Purview.ValueObjects.SourceGenerator.Common" />
</ItemGroup>

<ItemGroup>
<!-- The code-fix assembly shares the diagnostic descriptors and type library without
acquiring any Workspaces dependency in this assembly. -->
<InternalsVisibleTo Include="Purview.ValueObjects.SourceGenerator.Refactorings" />
</ItemGroup>
<!-- The code-fix assembly (SourceGenerator.Refactorings) consumes the diagnostic identities produced
here through public members, never through internals. Do not add an InternalsVisibleTo for it:
consumers receive the merged, self-contained analyzer the Purview.SourceGeneratorFramework merge
pass produces, and that pass strips every InternalsVisibleTo declaration, so internal coupling
compiles against the unmerged build output and then fails with FieldAccessException in the IDE. -->
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace Purview.ValueObjects.SourceGenerator.Refactorings;
/// Manual code-fix harness. The framework's code-fix test base does not run the source generators,
/// but the aggregate/value-object attributes used by the analyzers are emitted by those generators,
/// so this harness runs them before analyzing.
/// <para>
/// The generic overloads construct the in-repo components. The instance overloads accept components
/// supplied by the caller, which lets a test drive the packaged, merged analyzer artifact that
/// consumers and Visual Studio actually load; see <see cref="PackagedAnalyzerComponents"/>.
/// </para>
/// </summary>
public static class CodeFixTestHarness
{
Expand All @@ -20,33 +25,67 @@ CancellationToken cancellationToken
)
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new() =>
ApplyAsync<TAnalyzer, TCodeFix>(source, fixAll: false, cancellationToken);
RunAsync(
source,
new Generators.ValueObjectSourceGenerator(),
new TAnalyzer(),
new TCodeFix(),
fixAll: false,
cancellationToken
);

public static Task<HarnessResult> ApplyFixAllAsync<TAnalyzer, TCodeFix>(
string source,
CancellationToken cancellationToken
)
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new() =>
ApplyAsync<TAnalyzer, TCodeFix>(source, fixAll: true, cancellationToken);
RunAsync(
source,
new Generators.ValueObjectSourceGenerator(),
new TAnalyzer(),
new TCodeFix(),
fixAll: true,
cancellationToken
);

/// <summary>
/// Applies the first registered code fix using components supplied by the caller, so a test can drive
/// a packaged (merged, self-contained) analyzer artifact instead of the in-repo build output.
/// </summary>
public static Task<HarnessResult> ApplyAsync(
string source,
IIncrementalGenerator generator,
DiagnosticAnalyzer analyzer,
CodeFixProvider provider,
CancellationToken cancellationToken
) => RunAsync(source, generator, analyzer, provider, fixAll: false, cancellationToken);

/// <summary>Applies every registered code fix using components supplied by the caller.</summary>
public static Task<HarnessResult> ApplyFixAllAsync(
string source,
IIncrementalGenerator generator,
DiagnosticAnalyzer analyzer,
CodeFixProvider provider,
CancellationToken cancellationToken
) => RunAsync(source, generator, analyzer, provider, fixAll: true, cancellationToken);

static async Task<HarnessResult> ApplyAsync<TAnalyzer, TCodeFix>(
static async Task<HarnessResult> RunAsync(
string source,
IIncrementalGenerator generator,
DiagnosticAnalyzer analyzer,
CodeFixProvider provider,
bool fixAll,
CancellationToken cancellationToken
)
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new()
{
var (updatedCompilation, originalTree) = CreateCompilation(source);
var (updatedCompilation, originalTree) = CreateCompilation(source, generator);

TAnalyzer analyzer = new();
var analyzerDiagnosticsAll = await updatedCompilation
.WithAnalyzers([analyzer])
.GetAnalyzerDiagnosticsAsync(cancellationToken);
var analyzerDiagnostics = analyzerDiagnosticsAll.ToArray();

TCodeFix provider = new();
var applicable = analyzerDiagnostics
.Where(diagnostic => provider.FixableDiagnosticIds.Contains(diagnostic.Id, StringComparer.Ordinal))
.ToArray();
Expand Down Expand Up @@ -100,7 +139,10 @@ [.. applicable.Select(static diagnostic => diagnostic.Id)]
);
}

static (Compilation Compilation, SyntaxTree OriginalTree) CreateCompilation(string source)
static (Compilation Compilation, SyntaxTree OriginalTree) CreateCompilation(
string source,
IIncrementalGenerator generator
)
{
CSharpParseOptions parseOptions = new(LanguageVersion.Latest);
var tree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "Test.cs");
Expand All @@ -115,9 +157,7 @@ [.. applicable.Select(static diagnostic => diagnostic.Id)]
);

// The value-object attributes are emitted by the source generator.
GeneratorDriver driver = CSharpGeneratorDriver.Create([
new Generators.ValueObjectSourceGenerator().AsSourceGenerator(),
]);
GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()]);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out _);

return (updatedCompilation, tree);
Expand Down
Loading
Loading