Skip to content

Commit b3d8861

Browse files
authored
Merge pull request #65 from EFNext/fix/harnessing
Various identified issues
2 parents 7ee8480 + 714aad8 commit b3d8861

22 files changed

Lines changed: 936 additions & 123 deletions

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="8.0.25" />
3333
<PackageVersion Include="MongoDB.Driver" Version="3.0.0" />
3434
<PackageVersion Include="Snappier" Version="1.3.1" />
35+
<!-- Transitive pin: MongoDB.Driver 3.x pulls SharpCompress 0.30.1, which has GHSA-6c8g-7p36-r338 (<=0.47.4). -->
36+
<PackageVersion Include="SharpCompress" Version="0.48.1" />
3537
<PackageVersion Include="Testcontainers.MongoDb" Version="4.3.0" />
3638
<!-- Blazor WebAssembly host for the docs playground. -->
3739
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />

src/Docs/Prerenderer/ExpressiveSharp.Docs.Prerenderer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
VersionOverride="10.0.5" />
2020
<PackageReference Include="MongoDB.Driver" />
2121
<PackageReference Include="Snappier" />
22+
<PackageReference Include="SharpCompress" />
2223
</ItemGroup>
2324

2425
<ItemGroup>

src/ExpressiveSharp.CodeFixers/ExpressiveQueryableDropoutAnalyzer.cs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,17 @@ public override void Initialize(AnalysisContext context)
3838
{
3939
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
4040
context.EnableConcurrentExecution();
41-
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
41+
context.RegisterCompilationStartAction(compilationStart =>
42+
{
43+
var openGeneric = compilationStart.Compilation
44+
.GetTypeByMetadataName("ExpressiveSharp.IExpressiveQueryable`1");
45+
compilationStart.RegisterSyntaxNodeAction(
46+
ctx => AnalyzeInvocation(ctx, openGeneric),
47+
SyntaxKind.InvocationExpression);
48+
});
4249
}
4350

44-
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
51+
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressiveQueryableOpenGeneric)
4552
{
4653
var invocation = (InvocationExpressionSyntax)context.Node;
4754

@@ -81,13 +88,66 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
8188
if (ExpressiveSymbolHelpers.IsOrImplementsExpressiveQueryable(resultType))
8289
return;
8390

91+
// Suppress when an IExpressiveQueryable<T> sibling exists in any referenced namespace
92+
// — that scenario is owned by EXP0021 (a higher-severity Warning with its own codefix
93+
// for adding the missing `using`). Reporting both would just be duplicate noise.
94+
if (expressiveQueryableOpenGeneric is not null
95+
&& FindExpressiveSiblingNamespace(context.SemanticModel.Compilation, calledName, expressiveQueryableOpenGeneric) is not null)
96+
{
97+
return;
98+
}
99+
84100
context.ReportDiagnostic(Diagnostic.Create(
85101
ExpressiveQueryableDropout,
86102
memberAccess.Name.GetLocation(),
87103
properties: null,
88104
calledName));
89105
}
90106

107+
/// <summary>
108+
/// Walks the compilation's referenced assemblies looking for an extension method
109+
/// named <paramref name="methodName"/> whose first parameter is
110+
/// <c>IExpressiveQueryable&lt;T&gt;</c>. Returns the containing namespace's display
111+
/// string when found — used to suggest a `using` directive that would bring the
112+
/// sibling overload into scope.
113+
/// </summary>
114+
private static string? FindExpressiveSiblingNamespace(Compilation compilation, string methodName, INamedTypeSymbol expressiveQueryableOpenGeneric)
115+
{
116+
foreach (var reference in compilation.References)
117+
{
118+
if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly)
119+
continue;
120+
var match = SearchNamespace(assembly.GlobalNamespace, methodName, expressiveQueryableOpenGeneric);
121+
if (match is not null) return match;
122+
}
123+
return SearchNamespace(compilation.Assembly.GlobalNamespace, methodName, expressiveQueryableOpenGeneric);
124+
}
125+
126+
private static string? SearchNamespace(INamespaceSymbol ns, string methodName, INamedTypeSymbol expressiveQueryableOpenGeneric)
127+
{
128+
foreach (var type in ns.GetTypeMembers())
129+
{
130+
if (!type.IsStatic) continue;
131+
foreach (var member in type.GetMembers(methodName))
132+
{
133+
if (member is IMethodSymbol method
134+
&& method.IsExtensionMethod
135+
&& method.Parameters.Length > 0
136+
&& method.Parameters[0].Type is INamedTypeSymbol firstParamType
137+
&& SymbolEqualityComparer.Default.Equals(firstParamType.ConstructedFrom, expressiveQueryableOpenGeneric))
138+
{
139+
return type.ContainingNamespace?.ToDisplayString();
140+
}
141+
}
142+
}
143+
foreach (var child in ns.GetNamespaceMembers())
144+
{
145+
var found = SearchNamespace(child, methodName, expressiveQueryableOpenGeneric);
146+
if (found is not null) return found;
147+
}
148+
return null;
149+
}
150+
91151
private static bool ImplementsIQueryable(ITypeSymbol type)
92152
{
93153
if (IsIQueryable(type))

src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ private string EmitOperation(IOperation operation)
495495
ICoalesceOperation coalesce => EmitCoalesce(coalesce),
496496
IArrayCreationOperation arrayCreate => EmitArrayCreation(arrayCreate),
497497
IArrayElementReferenceOperation arrayElement => EmitArrayElementReference(arrayElement),
498+
IImplicitIndexerReferenceOperation implicitIdx => EmitImplicitIndexerReference(implicitIdx),
498499
IAnonymousFunctionOperation lambda => EmitNestedLambda(lambda),
499500
IDelegateCreationOperation delegateCreate => EmitDelegateCreation(delegateCreate),
500501
ITupleOperation tuple => EmitTuple(tuple),
@@ -1071,9 +1072,13 @@ private string EmitConversion(IConversionOperation conversion)
10711072
return quoteResult;
10721073
}
10731074

1074-
// Implicit reference upcasts in argument position break EF Core's queryable-chain matching.
1075-
if (conversion.IsImplicit && conversion.Conversion.IsReference && conversion.Parent is IArgumentOperation)
1075+
// Implicit reference upcasts from a concrete reference operands
1076+
if (conversion.IsImplicit
1077+
&& conversion.Conversion.IsReference
1078+
&& conversion.Operand.Type is { IsReferenceType: true })
1079+
{
10761080
return EmitOperation(conversion.Operand);
1081+
}
10771082

10781083
var resultVar = NextVar();
10791084
var operandVar = EmitOperation(conversion.Operand);
@@ -1634,6 +1639,23 @@ private string EmitDeclarationPattern(IDeclarationPatternOperation declaration,
16341639
return resultVar;
16351640
}
16361641

1642+
// Switch arm bodies can reference pattern-declared variables (`int i => i + 1`).
1643+
// The pattern itself emits only the TypeIs/Equal/etc. test, so we bind each
1644+
// declared local to a Convert of the governing value before the arm body emits,
1645+
// otherwise the local reference falls through to the closure-capture path and
1646+
// tries to read a non-existent field on __func.Target.
1647+
private void BindPatternDeclarations(IPatternOperation pattern, string operandVar)
1648+
{
1649+
if (pattern is IDeclarationPatternOperation decl
1650+
&& decl.DeclaredSymbol is ILocalSymbol localSym)
1651+
{
1652+
var convertVar = NextVar();
1653+
var typeFqn = decl.NarrowedType.ToDisplayString(_fqnFormat);
1654+
AppendLine($"var {convertVar} = {Expr}.Convert({operandVar}, typeof({typeFqn}));");
1655+
_localToVar[localSym] = convertVar;
1656+
}
1657+
}
1658+
16371659
private string EmitRelationalPattern(IRelationalPatternOperation relational, string operandVar, ITypeSymbol? operandType)
16381660
{
16391661
var resultVar = NextVar();
@@ -1732,31 +1754,44 @@ private string EmitListPattern(IListPatternOperation listPattern, string operand
17321754
{
17331755
var conditions = new List<string>();
17341756

1735-
var countProp = operandType?.GetMembers("Count").OfType<IPropertySymbol>().FirstOrDefault()
1736-
?? operandType?.GetMembers("Length").OfType<IPropertySymbol>().FirstOrDefault();
1757+
var arrayType = operandType as IArrayTypeSymbol;
1758+
IPropertySymbol? countProp = null;
1759+
IPropertySymbol? indexer = null;
17371760

1738-
var indexer = operandType?.GetMembers()
1739-
.OfType<IPropertySymbol>()
1740-
.FirstOrDefault(p => p.IsIndexer && p.Parameters.Length == 1
1741-
&& p.Parameters[0].Type.SpecialType == SpecialType.System_Int32);
1742-
1743-
if (countProp is null || indexer is null)
1761+
if (arrayType is null)
17441762
{
1745-
ReportDiagnostic(Diagnostics.UnsupportedOperation,
1746-
listPattern.Syntax?.GetLocation() ?? Location.None,
1747-
"ListPattern (type lacks Count/Length or indexer)");
1748-
return EmitUnsupported(listPattern);
1749-
}
1763+
countProp = operandType?.GetMembers("Count").OfType<IPropertySymbol>().FirstOrDefault()
1764+
?? operandType?.GetMembers("Length").OfType<IPropertySymbol>().FirstOrDefault();
17501765

1751-
var countField = _fieldCache.EnsurePropertyInfo(countProp);
1766+
indexer = operandType?.GetMembers()
1767+
.OfType<IPropertySymbol>()
1768+
.FirstOrDefault(p => p.IsIndexer && p.Parameters.Length == 1
1769+
&& p.Parameters[0].Type.SpecialType == SpecialType.System_Int32);
1770+
1771+
if (countProp is null || indexer is null)
1772+
{
1773+
ReportDiagnostic(Diagnostics.UnsupportedOperation,
1774+
listPattern.Syntax?.GetLocation() ?? Location.None,
1775+
"ListPattern (type lacks Count/Length or indexer)");
1776+
return EmitUnsupported(listPattern);
1777+
}
1778+
}
17521779

17531780
// A `..` slice means minimum-length match; otherwise exact-length.
17541781
var hasSlice = listPattern.Patterns.Any(p => p is ISlicePatternOperation);
17551782
var fixedPatterns = listPattern.Patterns.Where(p => p is not ISlicePatternOperation).ToList();
17561783
var requiredCount = fixedPatterns.Count;
17571784

17581785
var countAccess = NextVar();
1759-
AppendLine($"var {countAccess} = {Expr}.Property({operandVar}, {countField});");
1786+
if (arrayType is not null)
1787+
{
1788+
AppendLine($"var {countAccess} = {Expr}.ArrayLength({operandVar});");
1789+
}
1790+
else
1791+
{
1792+
var countField = _fieldCache.EnsurePropertyInfo(countProp!);
1793+
AppendLine($"var {countAccess} = {Expr}.Property({operandVar}, {countField});");
1794+
}
17601795
var countConst = NextVar();
17611796
AppendLine($"var {countConst} = {Expr}.Constant({requiredCount});");
17621797
var lengthCheck = NextVar();
@@ -1788,17 +1823,19 @@ private string EmitListPattern(IListPatternOperation listPattern, string operand
17881823
AppendLine($"var {idxConst} = {Expr}.Constant({elementIndex});");
17891824

17901825
var elementAccess = NextVar();
1791-
if (operandType is IArrayTypeSymbol)
1826+
ITypeSymbol elementType;
1827+
if (arrayType is not null)
17921828
{
17931829
AppendLine($"var {elementAccess} = {Expr}.ArrayIndex({operandVar}, {idxConst});");
1830+
elementType = arrayType.ElementType;
17941831
}
17951832
else
17961833
{
1797-
var indexerField = _fieldCache.EnsurePropertyInfo(indexer);
1834+
var indexerField = _fieldCache.EnsurePropertyInfo(indexer!);
17981835
AppendLine($"var {elementAccess} = {Expr}.Property({operandVar}, {indexerField}, {idxConst});");
1836+
elementType = indexer!.Type;
17991837
}
18001838

1801-
var elementType = indexer.Type;
18021839
var subCondition = EmitPattern(subPattern, elementAccess, elementType);
18031840
conditions.Add(subCondition);
18041841
elementIndex++;
@@ -2134,6 +2171,8 @@ private string EmitSwitchExpression(ISwitchExpressionOperation switchExpr)
21342171

21352172
var conditionVar = EmitPattern(arm.Pattern, governingVar, switchExpr.Value.Type);
21362173

2174+
BindPatternDeclarations(arm.Pattern, governingVar);
2175+
21372176
if (arm.Guard is not null)
21382177
{
21392178
var guardVar = EmitOperation(arm.Guard);
@@ -2716,6 +2755,70 @@ private string EmitIndexFromEnd(IUnaryOperation unary)
27162755
return resultVar;
27172756
}
27182757

2758+
// Lowers `s[range]` on string to `s.Substring(start, length)` so the result lands
2759+
// in expression-tree shape (the language-level Range/Index machinery doesn't survive
2760+
// an expression tree otherwise).
2761+
private string EmitImplicitIndexerReference(IImplicitIndexerReferenceOperation op)
2762+
{
2763+
if (op.Instance is null || op.Instance.Type is null)
2764+
return EmitUnsupported(op);
2765+
2766+
var receiverType = op.Instance.Type;
2767+
var receiverVar = EmitOperation(op.Instance);
2768+
2769+
if (receiverType.SpecialType == SpecialType.System_String && op.Argument is IRangeOperation range)
2770+
{
2771+
var lengthAccessor = NextVar();
2772+
AppendLine($"var {lengthAccessor} = {Expr}.Property({receiverVar}, typeof(global::System.String).GetProperty(\"Length\"));");
2773+
2774+
var startVar = EmitIndexAsInt(range.LeftOperand, lengthAccessor, defaultIsZero: true);
2775+
var endVar = EmitIndexAsInt(range.RightOperand, lengthAccessor, defaultIsZero: false);
2776+
2777+
var lengthVar = NextVar();
2778+
AppendLine($"var {lengthVar} = {Expr}.Subtract({endVar}, {startVar});");
2779+
2780+
var substringMethod = NextVar();
2781+
AppendLine($"var {substringMethod} = typeof(global::System.String).GetMethod(\"Substring\", new global::System.Type[] {{ typeof(int), typeof(int) }});");
2782+
2783+
var resultVar = NextVar();
2784+
AppendLine($"var {resultVar} = {Expr}.Call({receiverVar}, {substringMethod}, {startVar}, {lengthVar});");
2785+
return resultVar;
2786+
}
2787+
2788+
return EmitUnsupported(op);
2789+
}
2790+
2791+
// Emits an int-typed expression representing the absolute offset of an Index operand.
2792+
// `defaultIsZero=true` returns 0 when the operand is omitted (left side of `..`);
2793+
// false returns the receiver length (right side).
2794+
private string EmitIndexAsInt(IOperation? indexOperand, string lengthVar, bool defaultIsZero)
2795+
{
2796+
if (indexOperand is null)
2797+
{
2798+
if (defaultIsZero)
2799+
{
2800+
var zeroVar = NextVar();
2801+
AppendLine($"var {zeroVar} = {Expr}.Constant(0);");
2802+
return zeroVar;
2803+
}
2804+
return lengthVar;
2805+
}
2806+
2807+
if (indexOperand is IUnaryOperation { OperatorKind: UnaryOperatorKind.Hat } fromEnd)
2808+
{
2809+
var inner = EmitOperation(fromEnd.Operand);
2810+
var resultVar = NextVar();
2811+
AppendLine($"var {resultVar} = {Expr}.Subtract({lengthVar}, {inner});");
2812+
return resultVar;
2813+
}
2814+
2815+
// Plain int (possibly wrapped in a conversion-to-Index that we can ignore).
2816+
if (indexOperand is IConversionOperation conv && conv.Operand.Type?.SpecialType == SpecialType.System_Int32)
2817+
return EmitOperation(conv.Operand);
2818+
2819+
return EmitOperation(indexOperand);
2820+
}
2821+
27192822
private string EmitRange(IRangeOperation range)
27202823
{
27212824
var resultVar = NextVar();
@@ -2856,7 +2959,10 @@ private string EmitCollectionExpression(ICollectionExpressionOperation collExpr)
28562959
if (type is IArrayTypeSymbol arrayType)
28572960
{
28582961
var elementTypeFqn = arrayType.ElementType.ToDisplayString(_fqnFormat);
2859-
AppendLine($"var {resultVar} = {Expr}.NewArrayInit(typeof({elementTypeFqn}), {elementsExpr});");
2962+
var arrayArgs = elementVars.Count == 0
2963+
? $"typeof({elementTypeFqn})"
2964+
: $"typeof({elementTypeFqn}), {elementsExpr}";
2965+
AppendLine($"var {resultVar} = {Expr}.NewArrayInit({arrayArgs});");
28602966
}
28612967
else if (type is INamedTypeSymbol namedType && namedType.IsGenericType
28622968
&& namedType.OriginalDefinition.SpecialType == SpecialType.None)
@@ -2872,7 +2978,7 @@ private string EmitCollectionExpression(ICollectionExpressionOperation collExpr)
28722978
.OfType<IMethodSymbol>()
28732979
.FirstOrDefault(m => m.Parameters.Length == 1);
28742980

2875-
if (addMethod is not null)
2981+
if (addMethod is not null && elementVars.Count > 0)
28762982
{
28772983
var addField = _fieldCache.EnsureMethodInfo(addMethod);
28782984
var elemInitVars = new List<string>();
@@ -2886,7 +2992,7 @@ private string EmitCollectionExpression(ICollectionExpressionOperation collExpr)
28862992
}
28872993
else
28882994
{
2889-
// No Add method — leave the collection empty.
2995+
// No elements (or no Add method)return the bare New expression.
28902996
AppendLine($"var {resultVar} = {newVar};");
28912997
}
28922998
}

src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ public string EnsurePropertyInfo(IPropertySymbol property)
2828

2929
public string EnsureFieldInfo(IFieldSymbol field)
3030
{
31-
var typeFqn = ResolveTypeFqn(field.ContainingType);
32-
var flags = field.IsStatic
31+
// Named tuple elements (X, Y) only exist at compile time; the runtime field on
32+
// ValueTuple<...> is Item1/Item2/etc. CorrespondingTupleField maps to that.
33+
var runtimeField = field.CorrespondingTupleField ?? field;
34+
var typeFqn = ResolveTypeFqn(runtimeField.ContainingType);
35+
var flags = runtimeField.IsStatic
3336
? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
3437
: "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
35-
return $"typeof({typeFqn}).GetField(\"{field.Name}\", {flags})";
38+
return $"typeof({typeFqn}).GetField(\"{runtimeField.Name}\", {flags})";
3639
}
3740

3841
public string EnsureMethodInfo(IMethodSymbol method)

0 commit comments

Comments
 (0)