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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="8.0.25" />
<PackageVersion Include="MongoDB.Driver" Version="3.0.0" />
<PackageVersion Include="Snappier" Version="1.3.1" />
<!-- Transitive pin: MongoDB.Driver 3.x pulls SharpCompress 0.30.1, which has GHSA-6c8g-7p36-r338 (<=0.47.4). -->
<PackageVersion Include="SharpCompress" Version="0.48.1" />
<PackageVersion Include="Testcontainers.MongoDb" Version="4.3.0" />
<!-- Blazor WebAssembly host for the docs playground. -->
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
VersionOverride="10.0.5" />
<PackageReference Include="MongoDB.Driver" />
<PackageReference Include="Snappier" />
<PackageReference Include="SharpCompress" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,17 @@ public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
context.RegisterCompilationStartAction(compilationStart =>
{
var openGeneric = compilationStart.Compilation
.GetTypeByMetadataName("ExpressiveSharp.IExpressiveQueryable`1");
compilationStart.RegisterSyntaxNodeAction(
ctx => AnalyzeInvocation(ctx, openGeneric),
SyntaxKind.InvocationExpression);
});
}

private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressiveQueryableOpenGeneric)
{
var invocation = (InvocationExpressionSyntax)context.Node;

Expand Down Expand Up @@ -81,13 +88,66 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
if (ExpressiveSymbolHelpers.IsOrImplementsExpressiveQueryable(resultType))
return;

// Suppress when an IExpressiveQueryable<T> sibling exists in any referenced namespace
// — that scenario is owned by EXP0021 (a higher-severity Warning with its own codefix
// for adding the missing `using`). Reporting both would just be duplicate noise.
if (expressiveQueryableOpenGeneric is not null
&& FindExpressiveSiblingNamespace(context.SemanticModel.Compilation, calledName, expressiveQueryableOpenGeneric) is not null)
{
return;
}

context.ReportDiagnostic(Diagnostic.Create(
ExpressiveQueryableDropout,
memberAccess.Name.GetLocation(),
properties: null,
calledName));
}

/// <summary>
/// Walks the compilation's referenced assemblies looking for an extension method
/// named <paramref name="methodName"/> whose first parameter is
/// <c>IExpressiveQueryable&lt;T&gt;</c>. Returns the containing namespace's display
/// string when found — used to suggest a `using` directive that would bring the
/// sibling overload into scope.
/// </summary>
private static string? FindExpressiveSiblingNamespace(Compilation compilation, string methodName, INamedTypeSymbol expressiveQueryableOpenGeneric)
{
foreach (var reference in compilation.References)
{
if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly)
continue;
var match = SearchNamespace(assembly.GlobalNamespace, methodName, expressiveQueryableOpenGeneric);
if (match is not null) return match;
}
return SearchNamespace(compilation.Assembly.GlobalNamespace, methodName, expressiveQueryableOpenGeneric);
}

private static string? SearchNamespace(INamespaceSymbol ns, string methodName, INamedTypeSymbol expressiveQueryableOpenGeneric)
{
foreach (var type in ns.GetTypeMembers())
{
if (!type.IsStatic) continue;
foreach (var member in type.GetMembers(methodName))
{
if (member is IMethodSymbol method
&& method.IsExtensionMethod
&& method.Parameters.Length > 0
&& method.Parameters[0].Type is INamedTypeSymbol firstParamType
&& SymbolEqualityComparer.Default.Equals(firstParamType.ConstructedFrom, expressiveQueryableOpenGeneric))
{
return type.ContainingNamespace?.ToDisplayString();
}
}
}
foreach (var child in ns.GetNamespaceMembers())
{
var found = SearchNamespace(child, methodName, expressiveQueryableOpenGeneric);
if (found is not null) return found;
}
return null;
}

private static bool ImplementsIQueryable(ITypeSymbol type)
{
if (IsIQueryable(type))
Expand Down
152 changes: 129 additions & 23 deletions src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ private string EmitOperation(IOperation operation)
ICoalesceOperation coalesce => EmitCoalesce(coalesce),
IArrayCreationOperation arrayCreate => EmitArrayCreation(arrayCreate),
IArrayElementReferenceOperation arrayElement => EmitArrayElementReference(arrayElement),
IImplicitIndexerReferenceOperation implicitIdx => EmitImplicitIndexerReference(implicitIdx),
IAnonymousFunctionOperation lambda => EmitNestedLambda(lambda),
IDelegateCreationOperation delegateCreate => EmitDelegateCreation(delegateCreate),
ITupleOperation tuple => EmitTuple(tuple),
Expand Down Expand Up @@ -1071,9 +1072,13 @@ private string EmitConversion(IConversionOperation conversion)
return quoteResult;
}

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

var resultVar = NextVar();
var operandVar = EmitOperation(conversion.Operand);
Expand Down Expand Up @@ -1634,6 +1639,23 @@ private string EmitDeclarationPattern(IDeclarationPatternOperation declaration,
return resultVar;
}

// Switch arm bodies can reference pattern-declared variables (`int i => i + 1`).
// The pattern itself emits only the TypeIs/Equal/etc. test, so we bind each
// declared local to a Convert of the governing value before the arm body emits,
// otherwise the local reference falls through to the closure-capture path and
// tries to read a non-existent field on __func.Target.
private void BindPatternDeclarations(IPatternOperation pattern, string operandVar)
{
if (pattern is IDeclarationPatternOperation decl
&& decl.DeclaredSymbol is ILocalSymbol localSym)
{
var convertVar = NextVar();
var typeFqn = decl.NarrowedType.ToDisplayString(_fqnFormat);
AppendLine($"var {convertVar} = {Expr}.Convert({operandVar}, typeof({typeFqn}));");
_localToVar[localSym] = convertVar;
}
}

private string EmitRelationalPattern(IRelationalPatternOperation relational, string operandVar, ITypeSymbol? operandType)
{
var resultVar = NextVar();
Expand Down Expand Up @@ -1732,31 +1754,44 @@ private string EmitListPattern(IListPatternOperation listPattern, string operand
{
var conditions = new List<string>();

var countProp = operandType?.GetMembers("Count").OfType<IPropertySymbol>().FirstOrDefault()
?? operandType?.GetMembers("Length").OfType<IPropertySymbol>().FirstOrDefault();
var arrayType = operandType as IArrayTypeSymbol;
IPropertySymbol? countProp = null;
IPropertySymbol? indexer = null;

var indexer = operandType?.GetMembers()
.OfType<IPropertySymbol>()
.FirstOrDefault(p => p.IsIndexer && p.Parameters.Length == 1
&& p.Parameters[0].Type.SpecialType == SpecialType.System_Int32);

if (countProp is null || indexer is null)
if (arrayType is null)
{
ReportDiagnostic(Diagnostics.UnsupportedOperation,
listPattern.Syntax?.GetLocation() ?? Location.None,
"ListPattern (type lacks Count/Length or indexer)");
return EmitUnsupported(listPattern);
}
countProp = operandType?.GetMembers("Count").OfType<IPropertySymbol>().FirstOrDefault()
?? operandType?.GetMembers("Length").OfType<IPropertySymbol>().FirstOrDefault();

var countField = _fieldCache.EnsurePropertyInfo(countProp);
indexer = operandType?.GetMembers()
.OfType<IPropertySymbol>()
.FirstOrDefault(p => p.IsIndexer && p.Parameters.Length == 1
&& p.Parameters[0].Type.SpecialType == SpecialType.System_Int32);

if (countProp is null || indexer is null)
{
ReportDiagnostic(Diagnostics.UnsupportedOperation,
listPattern.Syntax?.GetLocation() ?? Location.None,
"ListPattern (type lacks Count/Length or indexer)");
return EmitUnsupported(listPattern);
}
}

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

var countAccess = NextVar();
AppendLine($"var {countAccess} = {Expr}.Property({operandVar}, {countField});");
if (arrayType is not null)
{
AppendLine($"var {countAccess} = {Expr}.ArrayLength({operandVar});");
}
else
{
var countField = _fieldCache.EnsurePropertyInfo(countProp!);
AppendLine($"var {countAccess} = {Expr}.Property({operandVar}, {countField});");
}
var countConst = NextVar();
AppendLine($"var {countConst} = {Expr}.Constant({requiredCount});");
var lengthCheck = NextVar();
Expand Down Expand Up @@ -1788,17 +1823,19 @@ private string EmitListPattern(IListPatternOperation listPattern, string operand
AppendLine($"var {idxConst} = {Expr}.Constant({elementIndex});");

var elementAccess = NextVar();
if (operandType is IArrayTypeSymbol)
ITypeSymbol elementType;
if (arrayType is not null)
{
AppendLine($"var {elementAccess} = {Expr}.ArrayIndex({operandVar}, {idxConst});");
elementType = arrayType.ElementType;
}
else
{
var indexerField = _fieldCache.EnsurePropertyInfo(indexer);
var indexerField = _fieldCache.EnsurePropertyInfo(indexer!);
AppendLine($"var {elementAccess} = {Expr}.Property({operandVar}, {indexerField}, {idxConst});");
elementType = indexer!.Type;
}

var elementType = indexer.Type;
var subCondition = EmitPattern(subPattern, elementAccess, elementType);
conditions.Add(subCondition);
elementIndex++;
Expand Down Expand Up @@ -2134,6 +2171,8 @@ private string EmitSwitchExpression(ISwitchExpressionOperation switchExpr)

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

BindPatternDeclarations(arm.Pattern, governingVar);

if (arm.Guard is not null)
{
var guardVar = EmitOperation(arm.Guard);
Expand Down Expand Up @@ -2716,6 +2755,70 @@ private string EmitIndexFromEnd(IUnaryOperation unary)
return resultVar;
}

// Lowers `s[range]` on string to `s.Substring(start, length)` so the result lands
// in expression-tree shape (the language-level Range/Index machinery doesn't survive
// an expression tree otherwise).
private string EmitImplicitIndexerReference(IImplicitIndexerReferenceOperation op)
{
if (op.Instance is null || op.Instance.Type is null)
return EmitUnsupported(op);

var receiverType = op.Instance.Type;
var receiverVar = EmitOperation(op.Instance);

if (receiverType.SpecialType == SpecialType.System_String && op.Argument is IRangeOperation range)
{
var lengthAccessor = NextVar();
AppendLine($"var {lengthAccessor} = {Expr}.Property({receiverVar}, typeof(global::System.String).GetProperty(\"Length\"));");

var startVar = EmitIndexAsInt(range.LeftOperand, lengthAccessor, defaultIsZero: true);
var endVar = EmitIndexAsInt(range.RightOperand, lengthAccessor, defaultIsZero: false);

var lengthVar = NextVar();
AppendLine($"var {lengthVar} = {Expr}.Subtract({endVar}, {startVar});");

var substringMethod = NextVar();
AppendLine($"var {substringMethod} = typeof(global::System.String).GetMethod(\"Substring\", new global::System.Type[] {{ typeof(int), typeof(int) }});");

var resultVar = NextVar();
AppendLine($"var {resultVar} = {Expr}.Call({receiverVar}, {substringMethod}, {startVar}, {lengthVar});");
return resultVar;
}

return EmitUnsupported(op);
}

// Emits an int-typed expression representing the absolute offset of an Index operand.
// `defaultIsZero=true` returns 0 when the operand is omitted (left side of `..`);
// false returns the receiver length (right side).
private string EmitIndexAsInt(IOperation? indexOperand, string lengthVar, bool defaultIsZero)
{
if (indexOperand is null)
{
if (defaultIsZero)
{
var zeroVar = NextVar();
AppendLine($"var {zeroVar} = {Expr}.Constant(0);");
return zeroVar;
}
return lengthVar;
}

if (indexOperand is IUnaryOperation { OperatorKind: UnaryOperatorKind.Hat } fromEnd)
{
var inner = EmitOperation(fromEnd.Operand);
var resultVar = NextVar();
AppendLine($"var {resultVar} = {Expr}.Subtract({lengthVar}, {inner});");
return resultVar;
}

// Plain int (possibly wrapped in a conversion-to-Index that we can ignore).
if (indexOperand is IConversionOperation conv && conv.Operand.Type?.SpecialType == SpecialType.System_Int32)
return EmitOperation(conv.Operand);

return EmitOperation(indexOperand);
}

private string EmitRange(IRangeOperation range)
{
var resultVar = NextVar();
Expand Down Expand Up @@ -2856,7 +2959,10 @@ private string EmitCollectionExpression(ICollectionExpressionOperation collExpr)
if (type is IArrayTypeSymbol arrayType)
{
var elementTypeFqn = arrayType.ElementType.ToDisplayString(_fqnFormat);
AppendLine($"var {resultVar} = {Expr}.NewArrayInit(typeof({elementTypeFqn}), {elementsExpr});");
var arrayArgs = elementVars.Count == 0
? $"typeof({elementTypeFqn})"
: $"typeof({elementTypeFqn}), {elementsExpr}";
AppendLine($"var {resultVar} = {Expr}.NewArrayInit({arrayArgs});");
}
else if (type is INamedTypeSymbol namedType && namedType.IsGenericType
&& namedType.OriginalDefinition.SpecialType == SpecialType.None)
Expand All @@ -2872,7 +2978,7 @@ private string EmitCollectionExpression(ICollectionExpressionOperation collExpr)
.OfType<IMethodSymbol>()
.FirstOrDefault(m => m.Parameters.Length == 1);

if (addMethod is not null)
if (addMethod is not null && elementVars.Count > 0)
{
var addField = _fieldCache.EnsureMethodInfo(addMethod);
var elemInitVars = new List<string>();
Expand All @@ -2886,7 +2992,7 @@ private string EmitCollectionExpression(ICollectionExpressionOperation collExpr)
}
else
{
// No Add method — leave the collection empty.
// No elements (or no Add method)return the bare New expression.
AppendLine($"var {resultVar} = {newVar};");
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ public string EnsurePropertyInfo(IPropertySymbol property)

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

public string EnsureMethodInfo(IMethodSymbol method)
Expand Down
Loading
Loading