Skip to content

Commit 174a3e2

Browse files
Copilotdex3r
andcommitted
Address review: split files, extract helpers, rename CSharpAccessibilityKeyword
- Move LoadedAssemblyContext into its own file - Extract DiagnosticMessageHelper for testable error-joining - Extract AbstractionsAssemblyResolver from GeneratesMethodExecutionRuntime - Extract DataGeneratorsFactorySetup from GeneratesMethodExecutionRuntime - Merge FromOrEmpty into ToKeyword with defaultToPrivate parameter - Add DiagnosticMessageHelperTests (5 tests) - Update CSharpAccessibilityKeywordTests for renamed API (12 tests) Co-authored-by: dex3r <[email protected]> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/7c930db8-e3f7-42d9-98cd-41cf86cdc442
1 parent 804f216 commit 174a3e2

10 files changed

Lines changed: 372 additions & 200 deletions

EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,105 +7,105 @@ namespace EasySourceGenerators.GeneratorTests;
77
public class CSharpAccessibilityKeywordTests
88
{
99
// -----------------------------------------------------------------------
10-
// From (returns "private" as default)
10+
// ToKeyword with defaultToPrivate = true (default)
1111
// -----------------------------------------------------------------------
1212

1313
[Test]
14-
public void From_Public_ReturnsPublic()
14+
public void ToKeyword_Public_ReturnsPublic()
1515
{
16-
string result = CSharpAccessibilityKeyword.From(Accessibility.Public);
16+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Public);
1717

1818
Assert.That(result, Is.EqualTo("public"));
1919
}
2020

2121
[Test]
22-
public void From_Protected_ReturnsProtected()
22+
public void ToKeyword_Protected_ReturnsProtected()
2323
{
24-
string result = CSharpAccessibilityKeyword.From(Accessibility.Protected);
24+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Protected);
2525

2626
Assert.That(result, Is.EqualTo("protected"));
2727
}
2828

2929
[Test]
30-
public void From_Internal_ReturnsInternal()
30+
public void ToKeyword_Internal_ReturnsInternal()
3131
{
32-
string result = CSharpAccessibilityKeyword.From(Accessibility.Internal);
32+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Internal);
3333

3434
Assert.That(result, Is.EqualTo("internal"));
3535
}
3636

3737
[Test]
38-
public void From_ProtectedOrInternal_ReturnsProtectedInternal()
38+
public void ToKeyword_ProtectedOrInternal_ReturnsProtectedInternal()
3939
{
40-
string result = CSharpAccessibilityKeyword.From(Accessibility.ProtectedOrInternal);
40+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.ProtectedOrInternal);
4141

4242
Assert.That(result, Is.EqualTo("protected internal"));
4343
}
4444

4545
[Test]
46-
public void From_ProtectedAndInternal_ReturnsPrivateProtected()
46+
public void ToKeyword_ProtectedAndInternal_ReturnsPrivateProtected()
4747
{
48-
string result = CSharpAccessibilityKeyword.From(Accessibility.ProtectedAndInternal);
48+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.ProtectedAndInternal);
4949

5050
Assert.That(result, Is.EqualTo("private protected"));
5151
}
5252

5353
[Test]
54-
public void From_Private_ReturnsPrivate()
54+
public void ToKeyword_Private_ReturnsPrivate()
5555
{
56-
string result = CSharpAccessibilityKeyword.From(Accessibility.Private);
56+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Private);
5757

5858
Assert.That(result, Is.EqualTo("private"));
5959
}
6060

6161
[Test]
62-
public void From_NotApplicable_ReturnsPrivate()
62+
public void ToKeyword_NotApplicable_ReturnsPrivate()
6363
{
64-
string result = CSharpAccessibilityKeyword.From(Accessibility.NotApplicable);
64+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.NotApplicable);
6565

6666
Assert.That(result, Is.EqualTo("private"));
6767
}
6868

6969
// -----------------------------------------------------------------------
70-
// FromOrEmpty (returns "" as default)
70+
// ToKeyword with defaultToPrivate = false
7171
// -----------------------------------------------------------------------
7272

7373
[Test]
74-
public void FromOrEmpty_Public_ReturnsPublic()
74+
public void ToKeyword_DefaultToPrivateFalse_Public_ReturnsPublic()
7575
{
76-
string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Public);
76+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Public, defaultToPrivate: false);
7777

7878
Assert.That(result, Is.EqualTo("public"));
7979
}
8080

8181
[Test]
82-
public void FromOrEmpty_Protected_ReturnsProtected()
82+
public void ToKeyword_DefaultToPrivateFalse_Protected_ReturnsProtected()
8383
{
84-
string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Protected);
84+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Protected, defaultToPrivate: false);
8585

8686
Assert.That(result, Is.EqualTo("protected"));
8787
}
8888

8989
[Test]
90-
public void FromOrEmpty_Internal_ReturnsInternal()
90+
public void ToKeyword_DefaultToPrivateFalse_Internal_ReturnsInternal()
9191
{
92-
string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Internal);
92+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Internal, defaultToPrivate: false);
9393

9494
Assert.That(result, Is.EqualTo("internal"));
9595
}
9696

9797
[Test]
98-
public void FromOrEmpty_Private_ReturnsEmptyString()
98+
public void ToKeyword_DefaultToPrivateFalse_Private_ReturnsEmptyString()
9999
{
100-
string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Private);
100+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Private, defaultToPrivate: false);
101101

102102
Assert.That(result, Is.EqualTo(""));
103103
}
104104

105105
[Test]
106-
public void FromOrEmpty_NotApplicable_ReturnsEmptyString()
106+
public void ToKeyword_DefaultToPrivateFalse_NotApplicable_ReturnsEmptyString()
107107
{
108-
string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.NotApplicable);
108+
string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.NotApplicable, defaultToPrivate: false);
109109

110110
Assert.That(result, Is.EqualTo(""));
111111
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using EasySourceGenerators.Generators.IncrementalGenerators;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
5+
namespace EasySourceGenerators.GeneratorTests;
6+
7+
[TestFixture]
8+
public class DiagnosticMessageHelperTests
9+
{
10+
[Test]
11+
public void JoinErrorDiagnostics_EmptyList_ReturnsEmptyString()
12+
{
13+
string result = DiagnosticMessageHelper.JoinErrorDiagnostics(Array.Empty<Diagnostic>());
14+
15+
Assert.That(result, Is.EqualTo(""));
16+
}
17+
18+
[Test]
19+
public void JoinErrorDiagnostics_OnlyWarnings_ReturnsEmptyString()
20+
{
21+
Diagnostic[] diagnostics = new[]
22+
{
23+
CreateDiagnostic(DiagnosticSeverity.Warning, "This is a warning")
24+
};
25+
26+
string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
27+
28+
Assert.That(result, Is.EqualTo(""));
29+
}
30+
31+
[Test]
32+
public void JoinErrorDiagnostics_SingleError_ReturnsSingleMessage()
33+
{
34+
Diagnostic[] diagnostics = new[]
35+
{
36+
CreateDiagnostic(DiagnosticSeverity.Error, "Something went wrong")
37+
};
38+
39+
string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
40+
41+
Assert.That(result, Is.EqualTo("Something went wrong"));
42+
}
43+
44+
[Test]
45+
public void JoinErrorDiagnostics_MultipleErrors_JoinsWithSemicolon()
46+
{
47+
Diagnostic[] diagnostics = new[]
48+
{
49+
CreateDiagnostic(DiagnosticSeverity.Error, "First error"),
50+
CreateDiagnostic(DiagnosticSeverity.Error, "Second error")
51+
};
52+
53+
string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
54+
55+
Assert.That(result, Is.EqualTo("First error; Second error"));
56+
}
57+
58+
[Test]
59+
public void JoinErrorDiagnostics_MixedSeverities_ReturnsOnlyErrors()
60+
{
61+
Diagnostic[] diagnostics = new[]
62+
{
63+
CreateDiagnostic(DiagnosticSeverity.Warning, "A warning"),
64+
CreateDiagnostic(DiagnosticSeverity.Error, "An error"),
65+
CreateDiagnostic(DiagnosticSeverity.Info, "An info")
66+
};
67+
68+
string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
69+
70+
Assert.That(result, Is.EqualTo("An error"));
71+
}
72+
73+
private static Diagnostic CreateDiagnostic(DiagnosticSeverity severity, string message)
74+
{
75+
DiagnosticDescriptor descriptor = new(
76+
id: "TEST001",
77+
title: "Test",
78+
messageFormat: message,
79+
category: "Test",
80+
defaultSeverity: severity,
81+
isEnabledByDefault: true);
82+
83+
return Diagnostic.Create(descriptor, Location.None);
84+
}
85+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Reflection;
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace EasySourceGenerators.Generators.IncrementalGenerators;
8+
9+
/// <summary>
10+
/// Resolves the EasySourceGenerators.Abstractions assembly from compilation references.
11+
/// Handles both file-based (<see cref="PortableExecutableReference"/>) and in-memory
12+
/// (<see cref="CompilationReference"/>) references, such as those provided by Rider's code inspector.
13+
/// </summary>
14+
internal static class AbstractionsAssemblyResolver
15+
{
16+
/// <summary>
17+
/// Resolves the abstractions assembly from the compilation references into the given
18+
/// <see cref="LoadedAssemblyContext"/>.
19+
/// </summary>
20+
internal static (Assembly? assembly, string? error) Resolve(
21+
LoadedAssemblyContext context,
22+
Compilation compilation)
23+
{
24+
MetadataReference[] matchingReferences = FindAbstractionsReferences(compilation);
25+
26+
if (matchingReferences.Length == 0)
27+
{
28+
return (null, BuildNoMatchError(compilation));
29+
}
30+
31+
PortableExecutableReference[] peReferences =
32+
matchingReferences.OfType<PortableExecutableReference>().ToArray();
33+
CompilationReference[] compilationReferences =
34+
matchingReferences.OfType<CompilationReference>().ToArray();
35+
36+
if (peReferences.Length > 0)
37+
{
38+
return LoadFromPortableExecutableReference(peReferences.First(), context);
39+
}
40+
41+
if (compilationReferences.Length > 0)
42+
{
43+
return LoadFromCompilationReference(context);
44+
}
45+
46+
string matchesString = string.Join(", ",
47+
matchingReferences.Select(reference =>
48+
$"{reference.Display} (type: {reference.GetType().Name})"));
49+
return (null,
50+
$"Found references matching '{Consts.AbstractionsAssemblyName}' but none were PortableExecutableReference or CompilationReference with valid file paths. \nMatching references: {matchesString}");
51+
}
52+
53+
/// <summary>
54+
/// Finds all compilation references that match the abstractions assembly name.
55+
/// </summary>
56+
private static MetadataReference[] FindAbstractionsReferences(Compilation compilation)
57+
{
58+
return compilation.References.Where(reference =>
59+
reference.Display is not null && (
60+
reference.Display.Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)
61+
|| (reference is PortableExecutableReference peRef && peRef.FilePath is not null &&
62+
Path.GetFileNameWithoutExtension(peRef.FilePath)
63+
.Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase))))
64+
.ToArray();
65+
}
66+
67+
/// <summary>
68+
/// Builds an error message when no matching abstractions reference is found.
69+
/// </summary>
70+
private static string BuildNoMatchError(Compilation compilation)
71+
{
72+
MetadataReference[] closestMatches = compilation.References.Where(reference =>
73+
reference.Display is not null
74+
&& reference.Display.Contains(Consts.SolutionNamespace, StringComparison.OrdinalIgnoreCase))
75+
.ToArray();
76+
77+
string closestMatchesString = string.Join(", ", closestMatches.Select(reference => reference.Display));
78+
79+
return $"Could not find any reference matching '{Consts.AbstractionsAssemblyName}' in compilation references.\n" +
80+
$" Found total references: {compilation.References.Count()}. \nMatching references: {closestMatches.Length}: \n{closestMatchesString}";
81+
}
82+
83+
/// <summary>
84+
/// Loads the abstractions assembly from a file-based <see cref="PortableExecutableReference"/>.
85+
/// </summary>
86+
private static (Assembly? assembly, string? error) LoadFromPortableExecutableReference(
87+
PortableExecutableReference reference,
88+
LoadedAssemblyContext context)
89+
{
90+
if (string.IsNullOrEmpty(reference.FilePath))
91+
{
92+
return (null,
93+
$"The reference matching '{Consts.AbstractionsAssemblyName}' does not have a valid file path.");
94+
}
95+
96+
string assemblyPath = GeneratorAssemblyExecutor.ResolveImplementationAssemblyPath(reference.FilePath);
97+
Assembly assembly = context.LoadContext.LoadFromAssemblyPath(assemblyPath);
98+
return (assembly, null);
99+
}
100+
101+
/// <summary>
102+
/// Loads the abstractions assembly from an in-memory <see cref="CompilationReference"/>.
103+
/// Uses the captured assembly from the load context if available, otherwise emits from bytes.
104+
/// </summary>
105+
private static (Assembly? assembly, string? error) LoadFromCompilationReference(
106+
LoadedAssemblyContext context)
107+
{
108+
if (context.CapturedAbstractionsAssembly != null)
109+
{
110+
return (context.CapturedAbstractionsAssembly, null);
111+
}
112+
113+
if (context.CompilationReferenceBytes.TryGetValue(Consts.AbstractionsAssemblyName,
114+
out byte[]? abstractionBytes))
115+
{
116+
Assembly assembly = context.LoadContext.LoadFromStream(new MemoryStream(abstractionBytes));
117+
return (assembly, null);
118+
}
119+
120+
return (null,
121+
$"Found reference matching '{Consts.AbstractionsAssemblyName}' as a CompilationReference, but failed to emit it to a loadable assembly.");
122+
}
123+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace EasySourceGenerators.Generators.IncrementalGenerators;
5+
6+
/// <summary>
7+
/// Sets up the <c>DataGeneratorsFactory</c> in the loaded execution assembly and
8+
/// assigns it to <c>Generate.CurrentGenerator</c> in the abstractions assembly,
9+
/// enabling fluent API usage during generator execution.
10+
/// </summary>
11+
internal static class DataGeneratorsFactorySetup
12+
{
13+
/// <summary>
14+
/// Creates a <c>DataGeneratorsFactory</c> instance and wires it to the
15+
/// <c>Generate.CurrentGenerator</c> static property. Returns an error message
16+
/// if the required types or properties cannot be found.
17+
/// </summary>
18+
internal static string? Setup(
19+
Assembly executionAssembly,
20+
Assembly abstractionsAssembly)
21+
{
22+
Type? generatorStaticType = abstractionsAssembly.GetType(Consts.GenerateTypeFullName);
23+
Type? dataGeneratorsFactoryType = executionAssembly.GetType(Consts.DataGeneratorsFactoryTypeFullName);
24+
if (generatorStaticType == null || dataGeneratorsFactoryType == null)
25+
{
26+
return
27+
$"Could not find {Consts.GenerateTypeFullName} or {Consts.DataGeneratorsFactoryTypeFullName} types in compiled assembly";
28+
}
29+
30+
object? dataGeneratorsFactory = Activator.CreateInstance(dataGeneratorsFactoryType);
31+
PropertyInfo? currentGeneratorProperty = generatorStaticType.GetProperty(
32+
Consts.CurrentGeneratorPropertyName, BindingFlags.NonPublic | BindingFlags.Static);
33+
currentGeneratorProperty?.SetValue(null, dataGeneratorsFactory);
34+
35+
return null;
36+
}
37+
}

0 commit comments

Comments
 (0)