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
72 changes: 57 additions & 15 deletions src/System.Management.Automation/engine/runtime/Binding/Binders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3928,6 +3928,8 @@ public override DynamicMetaObject FallbackGetIndex(DynamicMetaObject target, Dyn
return GetIndexArray(target, indexes, errorSuggestion).WriteToDebugLog(this);
}

var defaultMember = target.LimitType.GetCustomAttributes<DefaultMemberAttribute>(true).FirstOrDefault();
PropertyInfo lengthProperty = null;
foreach (var i in target.LimitType.GetInterfaces())
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>))
Expand All @@ -3938,12 +3940,23 @@ public override DynamicMetaObject FallbackGetIndex(DynamicMetaObject target, Dyn
return result.WriteToDebugLog(this);
}
}

// If the type explicitly implements an indexer specified by an interface
// then the DefaultMemberAttribute will not carry over to the implementation.
// This check will catch those cases.
if (defaultMember == null)
{
defaultMember = i.GetCustomAttributes<DefaultMemberAttribute>(inherit: false).FirstOrDefault();
if (defaultMember != null)
{
lengthProperty = i.GetProperty("Count") ?? i.GetProperty("Length");
}
}
}

var defaultMember = target.LimitType.GetCustomAttributes<DefaultMemberAttribute>(true).FirstOrDefault();
if (defaultMember != null)
{
return InvokeIndexer(target, indexes, errorSuggestion, defaultMember.MemberName).WriteToDebugLog(this);
return InvokeIndexer(target, indexes, errorSuggestion, defaultMember.MemberName, lengthProperty).WriteToDebugLog(this);
}

return errorSuggestion ?? CannotIndexTarget(target, indexes).WriteToDebugLog(this);
Expand Down Expand Up @@ -4027,9 +4040,29 @@ private DynamicMetaObject GetIndexDictionary(DynamicMetaObject target,
bindingRestrictions);
}

internal static bool CanIndexFromEndWithNegativeIndex(DynamicMetaObject target)
{
var limitType = target.LimitType;
internal static bool CanIndexFromEndWithNegativeIndex(
DynamicMetaObject target,
MethodInfo indexer,
ParameterInfo[] getterParams)
{
// PowerShell supports negative indexing for types that meet the following criteria:
// - Indexer method accepts one parameter that is typed as int
// - The int parameter is not a type argument from a constructed generic type
// (this is to exclude indexers for types that could use a negative index as
// a valid key like System.Linq.ILookup)
// - Declares a "Count" or "Length" property
// - Does not inherit from IDictionary<> as that is handled earlier in the binder
// For those types, generate special code to check for negative indices, otherwise just generate
// the call. Before we test for the above criteria explicitly, we will determine if the
// target is of a type known to be compatible. This is done to avoid the call to Module.ResolveMethod
// when possible.

if (getterParams.Length != 1 || getterParams[0].ParameterType != typeof(int))
{
return false;
}

Type limitType = target.LimitType;
if (limitType.IsArray || limitType == typeof(string) || limitType == typeof(StringBuilder))
{
return true;
Expand All @@ -4046,7 +4079,16 @@ internal static bool CanIndexFromEndWithNegativeIndex(DynamicMetaObject target)
}

// target implements IList<T>?
return limitType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>));
if (limitType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>)))
{
return true;
}

// Get the base method definition of the indexer to determine if the int
// parameter is a generic type parameter. Module.ResolveMethod is used
// because the indexer could be a method from a constructed generic type.
MethodBase baseMethod = indexer.Module.ResolveMethod(indexer.MetadataToken);
return !baseMethod.GetParameters()[0].ParameterType.IsGenericParameter;
}

private DynamicMetaObject IndexWithNegativeChecks(
Expand All @@ -4055,8 +4097,6 @@ private DynamicMetaObject IndexWithNegativeChecks(
PropertyInfo lengthProperty,
Func<Expression, Expression, Expression> generateIndexOperation)
{
Diagnostics.Assert(CanIndexFromEndWithNegativeIndex(target), "Unexpected target type to index from end with negative value");

// Generate:
// try {
// len = obj.Length
Expand Down Expand Up @@ -4197,7 +4237,8 @@ private DynamicMetaObject GetIndexMultiDimensionArray(DynamicMetaObject target,
private DynamicMetaObject InvokeIndexer(DynamicMetaObject target,
DynamicMetaObject[] indexes,
DynamicMetaObject errorSuggestion,
string methodName)
string methodName,
PropertyInfo lengthProperty)
{
MethodInfo getter = PSInvokeMemberBinder.FindBestMethod(target, indexes, "get_" + methodName, false, _constraints);

Expand Down Expand Up @@ -4258,13 +4299,14 @@ private DynamicMetaObject InvokeIndexer(DynamicMetaObject target,
target.CombineRestrictions(indexes));
}

if (getterParams.Length == 1 && getterParams[0].ParameterType == typeof(int) && CanIndexFromEndWithNegativeIndex(target))
if (CanIndexFromEndWithNegativeIndex(target, getter, getterParams))
{
// PowerShell supports negative indexing for some types (specifically, types implementing IList or IList<T>).
// For those types, generate special code to check for negative indices, otherwise just generate
// the call.
PropertyInfo lengthProperty = target.LimitType.GetProperty("Count") ??
target.LimitType.GetProperty("Length"); // for string
if (lengthProperty == null)
{
// Count is declared by most supported types, Length will catch some edge cases like strings.
lengthProperty = target.LimitType.GetProperty("Count") ??
target.LimitType.GetProperty("Length");
}

if (lengthProperty != null)
{
Expand Down
16 changes: 16 additions & 0 deletions test/powershell/Language/Scripting/Indexer.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,20 @@ Describe 'Tests for indexers' -Tags "CI" {
$service = Get-WmiObject -List -Amended Win32_Service
$service.Properties["Hello There"] | Should -BeNullOrEmpty
}

It 'ITuple implementations can be indexed' {
$tuple = [Tuple]::Create(10, 'Hello')
$tuple[0] | Should -Be 10
$tuple[1] | Should -BeExactly 'Hello'
}

It 'ITuple objects can be spliced' {
$tuple = [Tuple]::Create(10, 'Hello')
$tuple[0..1] | Should -Be @(10, 'Hello')
}

It 'Index of -1 should return the last item for ITuple objects' {
$tuple = [Tuple]::Create(10, 'Hello')
$tuple[-1] | Should -BeExactly 'Hello'
}
}