Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
147 changes: 101 additions & 46 deletions src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5422,7 +5422,7 @@ static BoundNode bindSpreadElement(SpreadElementSyntax syntax, BindingDiagnostic
Debug.Assert(length > 0);
lengthOrCount = new BoundLiteral(expression.Syntax, ConstantValue.Create(length), @this.GetSpecialType(SpecialType.System_Int32, diagnostics, expression.Syntax)) { WasCompilerGenerated = true };
}
else if (!@this.TryBindNonExtensionLengthOrCount(syntax.Expression, expressionPlaceholder, out lengthOrCount, ref useSiteInfo, diagnostics)) // PROTOTYPE should extension Length/Count count?
else if (!@this.TryBindNonExtensionLengthOrCount(syntax.Expression, expressionPlaceholder, out lengthOrCount, ref useSiteInfo, diagnostics))
{
lengthOrCount = null;
}
Expand Down Expand Up @@ -9268,17 +9268,15 @@ private bool TryBindImplicitIndexerInAnyScope(SyntaxNode syntax, BoundExpression

AnalyzedArguments? analyzedIntIndexerOrSliceArguments = null;
ImmutableArray<BoundImplicitIndexerValuePlaceholder> intIndexerOrSliceArgumentPlaceholders = default;
AnalyzedArguments? actualExtensionLengthOrCountArguments = null;
AnalyzedArguments? actualExtensionIntIndexerOrSliceArguments = null;

bool result = tryBindImplicitIndexerInAnyScope(
syntax, left, analyzedArguments, binder: this, ref useSiteInfo, diagnostics,
ref analyzedIntIndexerOrSliceArguments, ref intIndexerOrSliceArgumentPlaceholders,
ref actualExtensionLengthOrCountArguments, ref actualExtensionIntIndexerOrSliceArguments,
ref actualExtensionIntIndexerOrSliceArguments,
out extensionIndexerAccess);

analyzedIntIndexerOrSliceArguments?.Free();
actualExtensionLengthOrCountArguments?.Free();
actualExtensionIntIndexerOrSliceArguments?.Free();

return result;
Expand All @@ -9293,14 +9291,13 @@ static bool tryBindImplicitIndexerInAnyScope(
BindingDiagnosticBag diagnostics,
ref AnalyzedArguments? analyzedIntIndexerOrSliceArguments,
ref ImmutableArray<BoundImplicitIndexerValuePlaceholder> argumentPlaceholders,
ref AnalyzedArguments? actualExtensionLengthOrCountArguments,
ref AnalyzedArguments? actualExtensionIntIndexerOrSliceArguments,
out BoundExpression? indexerAccess)
{
indexerAccess = null;
IndexOrRangeArgKind argKind = GetIndexOrRangeArgKind(arguments, binder.Compilation);
if (argKind == IndexOrRangeArgKind.None)
{
indexerAccess = null;
return false;
}

Expand All @@ -9312,33 +9309,9 @@ static bool tryBindImplicitIndexerInAnyScope(
BoundExpression? indexerOrSliceAccess = null;
BoundExpression? lengthOrCountAccess = null;

bool foundApplicableLengthOrCount = false;
if (binder.TryBindNonExtensionLengthOrCount(syntax, receiverPlaceholder, out var instanceLengthOrCountAccess, ref useSiteInfo, implicitIndexerDiagnostics))
{
foundApplicableLengthOrCount = true;
lengthOrCountAccess = instanceLengthOrCountAccess;
}

if (!foundApplicableLengthOrCount)
{
foreach (var scope in new ExtensionScopes(binder))
{
if (tryLookupExtensionLengthOrCount(syntax, receiverPlaceholder, binder, scope,
ref actualExtensionLengthOrCountArguments, out PropertySymbol? extensionLengthOrCountProperty, ref useSiteInfo, implicitIndexerDiagnostics))
{
foundApplicableLengthOrCount = true;
if (extensionLengthOrCountProperty is not null)
{
Debug.Assert(extensionLengthOrCountProperty.ContainingType.ExtensionParameter is not null);
diagnostics.ReportUseSite(extensionLengthOrCountProperty, syntax);
lengthOrCountAccess = binder.GetExtensionMemberAccess(syntax, receiver, extensionLengthOrCountProperty, implicitIndexerDiagnostics).MakeCompilerGenerated();
lengthOrCountAccess = binder.CheckValue(lengthOrCountAccess, BindValueKind.RValue, implicitIndexerDiagnostics);
}

break;
}
}
}
// Check for Length/Count property in both instance and extension scopes
bool foundApplicableLengthOrCount = binder.TryBindLengthOrCountInAnyScope(
syntax, receiverPlaceholder, ref useSiteInfo, implicitIndexerDiagnostics, out lengthOrCountAccess);

bool foundApplicableIndexerOrSlice = false;
if (foundApplicableLengthOrCount)
Expand All @@ -9363,17 +9336,13 @@ static bool tryBindImplicitIndexerInAnyScope(
}
}
}
}

if (lengthOrCountAccess is not null && indexerOrSliceAccess is not null)
{
Debug.Assert(!argumentPlaceholders.IsDefault);
indexerAccess = binder.MakeImplicitIndexerAccess(syntax, receiver, arguments, receiverPlaceholder,
lengthOrCountAccess, indexerOrSliceAccess, argumentPlaceholders, argKind, implicitIndexerDiagnostics);
}
else
{
indexerAccess = null;
if (lengthOrCountAccess is not null && indexerOrSliceAccess is not null)
{
Debug.Assert(!argumentPlaceholders.IsDefault);
indexerAccess = binder.MakeImplicitIndexerAccess(syntax, receiver, arguments, receiverPlaceholder,
lengthOrCountAccess, indexerOrSliceAccess, argumentPlaceholders, argKind, implicitIndexerDiagnostics);
}
}

if (foundApplicableLengthOrCount && foundApplicableIndexerOrSlice)
Expand All @@ -9387,12 +9356,40 @@ static bool tryBindImplicitIndexerInAnyScope(
// (the Length/Count and the this[int]/Slice) when each part is searched independently across extension scopes.
return foundApplicableLengthOrCount && foundApplicableIndexerOrSlice;
}
}

// Returns true if any applicable candidates
// The caller is responsible to free actualExtensionLengthOrCountArguments
static bool TryBindExtensionLengthOrCountInScope(
SyntaxNode syntax,
BoundValuePlaceholderBase receiver,
Binder binder,
ExtensionScope scope,
ref AnalyzedArguments? actualExtensionLengthOrCountArguments,
out BoundExpression? lengthOrCountAccess,
ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo,
BindingDiagnosticBag diagnostics)
{
lengthOrCountAccess = null;
bool foundApplicable = tryLookupExtensionLengthOrCount(syntax, receiver, binder, scope, ref actualExtensionLengthOrCountArguments, out var lengthOrCountProperty, ref useSiteInfo, diagnostics);
if (foundApplicable)
{
if (lengthOrCountProperty is not null)
{
lengthOrCountProperty.AddUseSiteInfo(ref useSiteInfo);
Debug.Assert(lengthOrCountProperty.ContainingType.ExtensionParameter is not null);
lengthOrCountAccess = binder.GetExtensionMemberAccess(syntax, receiver, lengthOrCountProperty, diagnostics).MakeCompilerGenerated();
lengthOrCountAccess = binder.CheckValue(lengthOrCountAccess, BindValueKind.RValue, diagnostics);
}

return true;
}

return false;

// Returns true if any applicable candidates
// The caller is responsible to free actualExtensionLengthOrCountArguments
static bool tryLookupExtensionLengthOrCount(
SyntaxNode syntax,
BoundImplicitIndexerReceiverPlaceholder receiver,
BoundValuePlaceholderBase receiver,
Binder binder,
ExtensionScope scope,
ref AnalyzedArguments? actualExtensionLengthOrCountArguments,
Expand Down Expand Up @@ -9464,7 +9461,46 @@ static bool tryResolveLengthOrCount(BoundExpression receiver, ArrayBuilder<Prope
result.Free();
return foundApplicable;
}
}

private bool TryBindLengthOrCountInAnyScope(
SyntaxNode node,
BoundValuePlaceholderBase receiverPlaceholder,
ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo,
BindingDiagnosticBag diagnostics,
out BoundExpression? lengthAccess)
{
var instanceDiagnostics = BindingDiagnosticBag.GetInstance(diagnostics);
bool foundApplicable = TryBindNonExtensionLengthOrCount(node, receiverPlaceholder, out lengthAccess, ref useSiteInfo, instanceDiagnostics);

if (foundApplicable)
{
diagnostics.AddRangeAndFree(instanceDiagnostics);
return true;
}

AnalyzedArguments? actualExtensionLengthOrCountArguments = null;
foreach (var scope in new ExtensionScopes(this))
{
foundApplicable = TryBindExtensionLengthOrCountInScope(node, receiverPlaceholder, scope.Binder, scope,
Copy link
Member

@jjonescz jjonescz Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use this instead of scope.Binder here? #Resolved

ref actualExtensionLengthOrCountArguments, out lengthAccess, ref useSiteInfo, diagnostics);

if (foundApplicable)
{
if (lengthAccess is null)
{
break;
}

actualExtensionLengthOrCountArguments?.Free();
instanceDiagnostics.Free();
return true;
}
}

actualExtensionLengthOrCountArguments?.Free();
diagnostics.AddRangeAndFree(instanceDiagnostics);
return false;
}

// Returns true if any applicable candidates
Expand Down Expand Up @@ -11309,6 +11345,8 @@ private bool TryBindNonExtensionImplicitIndexerParts(
actualExtensionIntIndexerOrSliceArguments?.Free();
}

// We consider this scope to have an applicable implicit indexer if we found applicable candidates for both parts (the Length/Count and the this[int]/Slice).
// If only one parts or no parts are applicable, we'll continue searching further scopes.
return lengthOrCountAccess?.HasErrors == false && indexerOrSliceAccess?.HasErrors == false;
}

Expand Down Expand Up @@ -11408,13 +11446,30 @@ private static bool IsValidImplicitIndexIndexer(PropertySymbol property)
original.Parameters[0] is { Type.SpecialType: SpecialType.System_Int32, RefKind: RefKind.None };
}

// Returns true if any applicable candidates
private bool TryBindNonExtensionLengthOrCount(
SyntaxNode syntax,
BoundValuePlaceholderBase receiverPlaceholder,
out BoundExpression lengthOrCountAccess,
ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo,
BindingDiagnosticBag diagnostics)
{
Debug.Assert(receiverPlaceholder.Type is not null);
if (receiverPlaceholder.Type.IsSZArray())
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

receiverPlaceholder.Type.IsSZArray()

Is this ever true for non-list-pattern scenarios?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. There are three callers for TryBindNonExtensionLengthOrCount: 1. element access, 2. list-patterns, 3. spreads in collection expressions.
For indexer access, we cannot reach this because array access is treated separately (routed to BindArrayAccess).
But other scenarios can.

I'll add a targeted test (MissingMember_ArrayLength)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that a call site at bindSpreadElement now might result in this new behavior that couldn't happen before?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If behavior of MissingMember_ArrayLength in CiollectionExpressionTests.cs didn't change, then it feels like this special handling for an array is redundant, can be removed.

{
bool foundApplicable = TryGetSpecialTypeMember(Compilation, SpecialMember.System_Array__Length, syntax, diagnostics, out PropertySymbol lengthProperty);
if (lengthProperty is not null)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lengthProperty is not null

Can we assert that this value is equal to foundApplicable? #Closed

{
lengthOrCountAccess = new BoundPropertyAccess(syntax, receiverPlaceholder, initialBindingReceiverIsSubjectToCloning: ThreeState.False, lengthProperty, autoPropertyAccessorKind: AccessorKind.Unknown, LookupResultKind.Viable, lengthProperty.Type) { WasCompilerGenerated = true };
}
else
{
lengthOrCountAccess = new BoundBadExpression(syntax, LookupResultKind.Empty, ImmutableArray<Symbol?>.Empty, ImmutableArray<BoundExpression>.Empty, CreateErrorType(), hasErrors: true) { WasCompilerGenerated = true };
}

return foundApplicable;
}

var lookupResult = LookupResult.GetInstance();

Debug.Assert(receiverPlaceholder.Type is not null);
Expand Down
65 changes: 30 additions & 35 deletions src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -573,53 +573,48 @@ private bool IsCountableAndIndexable(SyntaxNode node, TypeSymbol inputType, out
}

private bool BindLengthAndIndexerForListPattern(SyntaxNode node, TypeSymbol inputType, BindingDiagnosticBag diagnostics,
out BoundExpression indexerAccess, out BoundExpression lengthAccess, out BoundListPatternReceiverPlaceholder? receiverPlaceholder, out BoundListPatternIndexPlaceholder argumentPlaceholder)
out BoundExpression indexerAccess, [NotNull] out BoundExpression? lengthAccess, out BoundListPatternReceiverPlaceholder? receiverPlaceholder, out BoundListPatternIndexPlaceholder argumentPlaceholder)
{
Debug.Assert(!inputType.IsDynamic());

bool hasErrors = false;
receiverPlaceholder = new BoundListPatternReceiverPlaceholder(node, inputType) { WasCompilerGenerated = true };
if (inputType.IsSZArray())
var useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
hasErrors = !TryBindLengthOrCountInAnyScope(node, receiverPlaceholder, ref useSiteInfo, diagnostics, out lengthAccess);
if (lengthAccess is null)
{
hasErrors |= !TryGetSpecialTypeMember(Compilation, SpecialMember.System_Array__Length, node, diagnostics, out PropertySymbol lengthProperty);
if (lengthProperty is not null)
{
lengthAccess = new BoundPropertyAccess(node, receiverPlaceholder, initialBindingReceiverIsSubjectToCloning: ThreeState.False, lengthProperty, autoPropertyAccessorKind: AccessorKind.Unknown, LookupResultKind.Viable, lengthProperty.Type) { WasCompilerGenerated = true };
}
else
{
lengthAccess = new BoundBadExpression(node, LookupResultKind.Empty, ImmutableArray<Symbol?>.Empty, ImmutableArray<BoundExpression>.Empty, CreateErrorType(), hasErrors: true) { WasCompilerGenerated = true };
}
}
else
{
var useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
if (!TryBindNonExtensionLengthOrCount(node, receiverPlaceholder, out lengthAccess, ref useSiteInfo, diagnostics)) // PROTOTYPE should extension Length/Count count?
{
hasErrors = true;
Error(diagnostics, ErrorCode.ERR_ListPatternRequiresLength, node, inputType);
}

diagnostics.Add(node, useSiteInfo);
hasErrors = true;
Error(diagnostics, ErrorCode.ERR_ListPatternRequiresLength, node, inputType);
lengthAccess = new BoundBadExpression(node, LookupResultKind.Empty, symbols: [], childBoundNodes: [], CreateErrorType(), hasErrors: true) { WasCompilerGenerated = true };
}

var analyzedArguments = AnalyzedArguments.GetInstance();
var systemIndexType = GetWellKnownType(WellKnownType.System_Index, diagnostics, node);
argumentPlaceholder = new BoundListPatternIndexPlaceholder(node, systemIndexType) { WasCompilerGenerated = true };
analyzedArguments.Arguments.Add(argumentPlaceholder);
diagnostics.Add(node, useSiteInfo);

indexerAccess = BindElementAccessCore(node, receiverPlaceholder, analyzedArguments, diagnostics).MakeCompilerGenerated();
indexerAccess = CheckValue(indexerAccess, BindValueKind.RValue, diagnostics);
Debug.Assert(indexerAccess is BoundIndexerAccess or BoundImplicitIndexerAccess or BoundArrayAccess or BoundBadExpression or BoundDynamicIndexerAccess or BoundPointerElementAccess);
analyzedArguments.Free();
hasErrors |= !tryBindIndexIndexer(receiverPlaceholder, out indexerAccess, out argumentPlaceholder, diagnostics, node);
return !hasErrors && !lengthAccess.HasErrors;

if (!systemIndexType.HasUseSiteError)
bool tryBindIndexIndexer(BoundListPatternReceiverPlaceholder receiverPlaceholder, out BoundExpression indexerAccess,
out BoundListPatternIndexPlaceholder argumentPlaceholder, BindingDiagnosticBag diagnostics,
SyntaxNode node)
{
// Check required well-known member.
_ = GetWellKnownTypeMember(WellKnownMember.System_Index__ctor, diagnostics, syntax: node);
}
var analyzedArguments = AnalyzedArguments.GetInstance();
var systemIndexType = GetWellKnownType(WellKnownType.System_Index, diagnostics, node);
argumentPlaceholder = new BoundListPatternIndexPlaceholder(node, systemIndexType) { WasCompilerGenerated = true };
analyzedArguments.Arguments.Add(argumentPlaceholder);

return !hasErrors && !lengthAccess.HasErrors && !indexerAccess.HasErrors;
indexerAccess = BindElementAccessCore(node, receiverPlaceholder, analyzedArguments, diagnostics).MakeCompilerGenerated();
indexerAccess = CheckValue(indexerAccess, BindValueKind.RValue, diagnostics);
Debug.Assert(indexerAccess is BoundIndexerAccess or BoundImplicitIndexerAccess or BoundArrayAccess or BoundBadExpression or BoundDynamicIndexerAccess or BoundPointerElementAccess);
analyzedArguments.Free();

if (!systemIndexType.HasUseSiteError)
{
// Check required well-known member.
_ = GetWellKnownTypeMember(WellKnownMember.System_Index__ctor, diagnostics, syntax: node);
}

return !indexerAccess.HasErrors;
}
}

private static BoundPattern BindDiscardPattern(DiscardPatternSyntax node, TypeSymbol inputType, BindingDiagnosticBag diagnostics)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,12 @@ private void VisitBinaryOperatorChildren(BoundBinaryOperatorBase node)
{
VisitList(node.Subpatterns);
Visit(node.VariableAccess);
// Ignore indexer access (just a node to hold onto some symbols)
if (!node.HasErrors &&
node.IndexerAccess is BoundIndexerAccess { Indexer: { } indexer } indexerAccess &&
indexer.IsExtensionBlockMember())
{
VisitList(indexerAccess.Arguments);
}
return null;
}

Expand Down
Loading