Skip to content

Commit ed7bc2a

Browse files
authored
Merge pull request #191 from EFNext/feat/source-generator-defaults
Implement global MSBuild defaults for [Projectable] options
2 parents a97ee28 + 9bd59e4 commit ed7bc2a

File tree

9 files changed

+459
-34
lines changed

9 files changed

+459
-34
lines changed

src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
<ItemGroup>
2828
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
29+
<None Include="build\EntityFrameworkCore.Projectables.Abstractions.props" Pack="true" PackagePath="buildTransitive\" />
2930
</ItemGroup>
3031

3132
</Project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project>
2+
<PropertyGroup>
3+
<!-- Global defaults for [Projectable] options. Override these in your project to set app-wide defaults.
4+
Per-member attribute settings always take precedence over these values. -->
5+
<Projectables_NullConditionalRewriteSupport
6+
Condition="'$(Projectables_NullConditionalRewriteSupport)' == ''" />
7+
<Projectables_ExpandEnumMethods
8+
Condition="'$(Projectables_ExpandEnumMethods)' == ''" />
9+
<Projectables_AllowBlockBody
10+
Condition="'$(Projectables_AllowBlockBody)' == ''" />
11+
</PropertyGroup>
12+
<ItemGroup>
13+
<CompilerVisibleProperty Include="Projectables_NullConditionalRewriteSupport" />
14+
<CompilerVisibleProperty Include="Projectables_ExpandEnumMethods" />
15+
<CompilerVisibleProperty Include="Projectables_AllowBlockBody" />
16+
</ItemGroup>
17+
</Project>

src/EntityFrameworkCore.Projectables.Generator/Comparers/MemberDeclarationSyntaxAndCompilationEqualityComparer.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
1-
using System.Runtime.CompilerServices;
1+
using System.Runtime.CompilerServices;
22
using EntityFrameworkCore.Projectables.Generator.Models;
33
using Microsoft.CodeAnalysis;
44
using Microsoft.CodeAnalysis.CSharp.Syntax;
55

66
namespace EntityFrameworkCore.Projectables.Generator.Comparers;
77

88
/// <summary>
9-
/// Equality comparer for tuples of (MemberDeclarationSyntax, ProjectableAttributeData) and Compilation,
9+
/// Equality comparer for tuples of (MemberDeclarationSyntax, ProjectableAttributeData, ProjectableGlobalOptions) and Compilation,
1010
/// used as keys in the registry to determine if a member's projectable status has changed across incremental generation steps.
1111
/// </summary>
1212
internal class MemberDeclarationSyntaxAndCompilationEqualityComparer
13-
: IEqualityComparer<((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation)>
13+
: IEqualityComparer<((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation)>
1414
{
1515
private readonly static MemberDeclarationSyntaxEqualityComparer _memberComparer = new();
1616

1717
public bool Equals(
18-
((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) x,
19-
((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) y)
18+
((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation) x,
19+
((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation) y)
2020
{
2121
var (xLeft, xCompilation) = x;
2222
var (yLeft, yCompilation) = y;
2323

2424
// 1. Fast reference equality short-circuit
2525
if (ReferenceEquals(xLeft.Member, yLeft.Member) &&
26-
ReferenceEquals(xCompilation, yCompilation))
26+
ReferenceEquals(xCompilation, yCompilation) &&
27+
xLeft.GlobalOptions == yLeft.GlobalOptions)
2728
{
2829
return true;
2930
}
@@ -43,17 +44,23 @@ public bool Equals(
4344
return false;
4445
}
4546

46-
// 4. Member text — string allocation, only reached when the SyntaxTree is shared
47+
// 4. Global options (primitive record struct) — cheap value comparison
48+
if (xLeft.GlobalOptions != yLeft.GlobalOptions)
49+
{
50+
return false;
51+
}
52+
53+
// 5. Member text — string allocation, only reached when the SyntaxTree is shared
4754
if (!_memberComparer.Equals(xLeft.Member, yLeft.Member))
4855
{
4956
return false;
5057
}
5158

52-
// 5. Assembly-level references — most expensive (ImmutableArray enumeration)
59+
// 6. Assembly-level references — most expensive (ImmutableArray enumeration)
5360
return xCompilation.ExternalReferences.SequenceEqual(yCompilation.ExternalReferences);
5461
}
5562

56-
public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) obj)
63+
public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation) obj)
5764
{
5865
var (left, compilation) = obj;
5966
unchecked
@@ -62,7 +69,8 @@ public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeDat
6269
hash = hash * 31 + _memberComparer.GetHashCode(left.Member);
6370
hash = hash * 31 + RuntimeHelpers.GetHashCode(left.Member.SyntaxTree);
6471
hash = hash * 31 + left.Attribute.GetHashCode();
65-
72+
hash = hash * 31 + left.GlobalOptions.GetHashCode();
73+
6674
// Incorporate compilation external references to align with Equals
6775
var references = compilation.ExternalReferences;
6876
var referencesHash = 17;
@@ -72,7 +80,7 @@ public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeDat
7280
referencesHash = referencesHash * 31 + RuntimeHelpers.GetHashCode(reference);
7381
}
7482
hash = hash * 31 + referencesHash;
75-
83+
7684
return hash;
7785
}
7886
}

src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ static internal partial class ProjectableInterpreter
1313
MemberDeclarationSyntax member,
1414
ISymbol memberSymbol,
1515
ProjectableAttributeData projectableAttribute,
16+
ProjectableGlobalOptions globalOptions,
1617
SourceProductionContext context,
1718
Compilation? compilation = null)
1819
{
19-
// Read directly from the struct fields
20-
var nullConditionalRewriteSupport = projectableAttribute.NullConditionalRewriteSupport;
20+
// Resolve effective values: per-attribute wins, then global MSBuild default, then hard-coded fallback.
21+
var nullConditionalRewriteSupport =
22+
projectableAttribute.NullConditionalRewriteSupport ?? globalOptions.NullConditionalRewriteSupport ?? default;
2123
var useMemberBody = projectableAttribute.UseMemberBody;
22-
var expandEnumMethods = projectableAttribute.ExpandEnumMethods;
23-
var allowBlockBody = projectableAttribute.AllowBlockBody;
24+
var expandEnumMethods =
25+
projectableAttribute.ExpandEnumMethods ?? globalOptions.ExpandEnumMethods ?? false;
26+
var allowBlockBody =
27+
projectableAttribute.AllowBlockBody ?? globalOptions.AllowBlockBody ?? false;
2428

2529
// 1. Resolve the member body (handles UseMemberBody redirection)
2630
var memberBody = TryResolveMemberBody(member, memberSymbol, useMemberBody, context);

src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@ namespace EntityFrameworkCore.Projectables.Generator.Models;
44

55
/// <summary>
66
/// Plain-data snapshot of the [Projectable] attribute arguments.
7+
/// Nullable option fields are <c>null</c> when the named argument was absent from the attribute,
8+
/// meaning the global MSBuild default (or hard-coded fallback) should be used instead.
79
/// </summary>
810
readonly internal record struct ProjectableAttributeData
911
{
10-
public NullConditionalRewriteSupport NullConditionalRewriteSupport { get; }
12+
public NullConditionalRewriteSupport? NullConditionalRewriteSupport { get; }
1113
public string? UseMemberBody { get; }
12-
public bool ExpandEnumMethods { get; }
13-
public bool AllowBlockBody { get; }
14-
14+
public bool? ExpandEnumMethods { get; }
15+
public bool? AllowBlockBody { get; }
16+
1517
public ProjectableAttributeData(AttributeData attribute)
1618
{
17-
var nullConditionalRewriteSupport = default(NullConditionalRewriteSupport);
19+
NullConditionalRewriteSupport? nullConditionalRewriteSupport = null;
1820
string? useMemberBody = null;
19-
var expandEnumMethods = false;
20-
var allowBlockBody = false;
21-
21+
bool? expandEnumMethods = null;
22+
bool? allowBlockBody = null;
23+
2224
foreach (var namedArgument in attribute.NamedArguments)
2325
{
2426
var key = namedArgument.Key;
@@ -40,23 +42,23 @@ value.Value is not null &&
4042
}
4143
break;
4244
case nameof(ExpandEnumMethods):
43-
if (value.Value is bool expand && expand)
45+
if (value.Value is bool expand)
4446
{
45-
expandEnumMethods = true;
47+
expandEnumMethods = expand;
4648
}
4749
break;
4850
case nameof(AllowBlockBody):
49-
if (value.Value is bool allow && allow)
51+
if (value.Value is bool allow)
5052
{
51-
allowBlockBody = true;
53+
allowBlockBody = allow;
5254
}
5355
break;
5456
}
5557
}
56-
58+
5759
NullConditionalRewriteSupport = nullConditionalRewriteSupport;
5860
UseMemberBody = useMemberBody;
5961
ExpandEnumMethods = expandEnumMethods;
6062
AllowBlockBody = allowBlockBody;
6163
}
62-
}
64+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Microsoft.CodeAnalysis.Diagnostics;
2+
3+
namespace EntityFrameworkCore.Projectables.Generator.Models;
4+
5+
/// <summary>
6+
/// Plain-data snapshot of the MSBuild global defaults for [Projectable] options.
7+
/// Read from <c>build_property.*</c> entries in <c>AnalyzerConfigOptions.GlobalOptions</c>.
8+
/// <c>null</c> means the property was not set (no global override).
9+
/// </summary>
10+
readonly internal record struct ProjectableGlobalOptions
11+
{
12+
public NullConditionalRewriteSupport? NullConditionalRewriteSupport { get; }
13+
public bool? ExpandEnumMethods { get; }
14+
public bool? AllowBlockBody { get; }
15+
16+
public ProjectableGlobalOptions(AnalyzerConfigOptions globalOptions)
17+
{
18+
if (globalOptions.TryGetValue("build_property.Projectables_NullConditionalRewriteSupport", out var nullConditionalStr)
19+
&& !string.IsNullOrEmpty(nullConditionalStr)
20+
&& Enum.TryParse<NullConditionalRewriteSupport>(nullConditionalStr, ignoreCase: true, out var nullConditional))
21+
{
22+
NullConditionalRewriteSupport = nullConditional;
23+
}
24+
25+
if (globalOptions.TryGetValue("build_property.Projectables_ExpandEnumMethods", out var expandStr)
26+
&& bool.TryParse(expandStr, out var expand))
27+
{
28+
ExpandEnumMethods = expand;
29+
}
30+
31+
if (globalOptions.TryGetValue("build_property.Projectables_AllowBlockBody", out var allowStr)
32+
&& bool.TryParse(allowStr, out var allow))
33+
{
34+
AllowBlockBody = allow;
35+
}
36+
}
37+
}

src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public class ProjectionExpressionGenerator : IIncrementalGenerator
3838

3939
public void Initialize(IncrementalGeneratorInitializationContext context)
4040
{
41+
// Snapshot global MSBuild defaults once per generator run.
42+
var globalOptions = context.AnalyzerConfigOptionsProvider
43+
.Select(static (opts, _) => new ProjectableGlobalOptions(opts.GlobalOptions));
44+
4145
// Extract only pure stable data from the attribute in the transform.
4246
// No live Roslyn objects (no AttributeData, SemanticModel, Compilation, ISymbol) —
4347
// those are always new instances and defeat incremental caching entirely.
@@ -50,14 +54,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5054
Attribute: new ProjectableAttributeData(c.Attributes[0])
5155
));
5256

53-
var compilationAndMemberPairs = memberDeclarations
57+
// Flatten (Member, Attribute) + GlobalOptions into a single named tuple.
58+
var memberDeclarationsWithGlobalOptions = memberDeclarations
59+
.Combine(globalOptions)
60+
.Select(static (pair, _) => (
61+
Member: pair.Left.Member,
62+
Attribute: pair.Left.Attribute,
63+
GlobalOptions: pair.Right
64+
));
65+
66+
var compilationAndMemberPairs = memberDeclarationsWithGlobalOptions
5467
.Combine(context.CompilationProvider)
5568
.WithComparer(new MemberDeclarationSyntaxAndCompilationEqualityComparer());
5669

5770
context.RegisterSourceOutput(compilationAndMemberPairs,
5871
static (spc, source) =>
5972
{
60-
var ((member, attribute), compilation) = source;
73+
var ((member, attribute, globalOptions), compilation) = source;
6174
var semanticModel = compilation.GetSemanticModel(member.SyntaxTree);
6275
var memberSymbol = semanticModel.GetDeclaredSymbol(member);
6376

@@ -66,13 +79,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6679
return;
6780
}
6881

69-
Execute(member, semanticModel, memberSymbol, attribute, compilation, spc);
82+
Execute(member, semanticModel, memberSymbol, attribute, globalOptions, compilation, spc);
7083
});
7184

7285
// Build the projection registry: collect all entries and emit a single registry file
7386
var registryEntries = compilationAndMemberPairs.Select(
7487
static (source, cancellationToken) => {
75-
var ((member, _), compilation) = source;
88+
var ((member, _, _), compilation) = source;
7689

7790
var semanticModel = compilation.GetSemanticModel(member.SyntaxTree);
7891
var memberSymbol = semanticModel.GetDeclaredSymbol(member, cancellationToken);
@@ -170,11 +183,12 @@ private static void Execute(
170183
SemanticModel semanticModel,
171184
ISymbol memberSymbol,
172185
ProjectableAttributeData projectableAttribute,
186+
ProjectableGlobalOptions globalOptions,
173187
Compilation? compilation,
174188
SourceProductionContext context)
175189
{
176190
var projectable = ProjectableInterpreter.GetDescriptor(
177-
semanticModel, member, memberSymbol, projectableAttribute, context, compilation);
191+
semanticModel, member, memberSymbol, projectableAttribute, globalOptions, context, compilation);
178192

179193
if (projectable is null)
180194
{

0 commit comments

Comments
 (0)