diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 76be6418805..7fbd7b751b3 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -2102,6 +2102,14 @@ private List GetResultForIdentifier(CompletionContext completi result = CompletionCompleters.CompleteCommandArgument(completionContext); replacementIndex = completionContext.ReplacementIndex; replacementLength = completionContext.ReplacementLength; + + if (result.Count == 0 + && completionContext.TokenAtCursor.TokenFlags.HasFlag(TokenFlags.TypeName) + && lastAst?.Find(a => a is MemberExpressionAst, searchNestedScriptBlocks: false) is not null) + { + result = CompletionCompleters.CompleteType(completionContext.TokenAtCursor.Text).ToList(); + } + return result; } diff --git a/src/System.Management.Automation/engine/CoreAdapter.cs b/src/System.Management.Automation/engine/CoreAdapter.cs index bf9775b35ef..86ed988362e 100644 --- a/src/System.Management.Automation/engine/CoreAdapter.cs +++ b/src/System.Management.Automation/engine/CoreAdapter.cs @@ -1394,32 +1394,37 @@ private static MethodInformation FindBestMethodImpl( // be turned into an array. // We also skip the optimization if the number of arguments and parameters is different // so we let the loop deal with possible optional parameters. - if ((methods.Length == 1) && - (!methods[0].hasVarArgs) && - (!methods[0].isGeneric) && - (methods[0].method == null || !(methods[0].method.DeclaringType.IsGenericTypeDefinition)) && + if (methods.Length == 1 + && !methods[0].hasVarArgs // generic methods need to be double checked in a loop below - generic methods can be rejected if type inference fails - (methods[0].parameters.Length == arguments.Length)) + && !methods[0].isGeneric + && (methods[0].method is null || !methods[0].method.DeclaringType.IsGenericTypeDefinition) + && methods[0].parameters.Length == arguments.Length) { return methods[0]; } Type[] argumentTypes = arguments.Select(EffectiveArgumentType).ToArray(); + Type[] genericParameters = invocationConstraints?.GenericTypeParameters ?? Array.Empty(); List candidates = new List(); for (int i = 0; i < methods.Length; i++) { - MethodInformation method = methods[i]; + MethodInformation methodInfo = methods[i]; - if (method.method != null && method.method.DeclaringType.IsGenericTypeDefinition) + if (methodInfo.method?.DeclaringType.IsGenericTypeDefinition == true + || (!methodInfo.isGeneric && genericParameters.Length > 0)) { - continue; // skip methods defined by an *open* generic type + // If method is defined by an *open* generic type, or + // if generic parameters were provided and this method isn't generic, skip it. + continue; } - if (method.isGeneric) + if (methodInfo.isGeneric) { Type[] argumentTypesForTypeInference = new Type[argumentTypes.Length]; Array.Copy(argumentTypes, argumentTypesForTypeInference, argumentTypes.Length); - if (invocationConstraints != null && invocationConstraints.ParameterTypes != null) + + if (invocationConstraints?.ParameterTypes is not null) { int parameterIndex = 0; foreach (Type typeConstraintFromCallSite in invocationConstraints.ParameterTypes) @@ -1433,20 +1438,36 @@ private static MethodInformation FindBestMethodImpl( } } - method = TypeInference.Infer(method, argumentTypesForTypeInference); - if (method == null) + if (genericParameters.Length > 0 && methodInfo.method is MethodInfo originalMethod) + { + try + { + methodInfo = new MethodInformation( + originalMethod.MakeGenericMethod(genericParameters), + parametersToIgnore: 0); + } + catch (ArgumentException) + { + // Just skip this possibility if the generic type parameters can't be used to make + // a valid generic method here. + continue; + } + } + + methodInfo = TypeInference.Infer(methodInfo, argumentTypesForTypeInference); + if (methodInfo is null) { // Skip generic methods for which we cannot infer type arguments continue; } } - if (!IsInvocationTargetConstraintSatisfied(method, invocationConstraints)) + if (!IsInvocationTargetConstraintSatisfied(methodInfo, invocationConstraints)) { continue; } - ParameterInformation[] parameters = method.parameters; + ParameterInformation[] parameters = methodInfo.parameters; if (arguments.Length != parameters.Length) { // Skip methods w/ an incorrect # of arguments. @@ -1454,7 +1475,7 @@ private static MethodInformation FindBestMethodImpl( if (arguments.Length > parameters.Length) { // If too many args,it's only OK if the method is varargs. - if (!method.hasVarArgs) + if (!methodInfo.hasVarArgs) { continue; } @@ -1462,12 +1483,12 @@ private static MethodInformation FindBestMethodImpl( else { // Too few args, OK if there are optionals, or varargs with the param array omitted - if (!method.hasOptional && (!method.hasVarArgs || (arguments.Length + 1) != parameters.Length)) + if (!methodInfo.hasOptional && (!methodInfo.hasVarArgs || (arguments.Length + 1) != parameters.Length)) { continue; } - if (method.hasOptional) + if (methodInfo.hasOptional) { // Count optionals. This code is rarely hit, mainly when calling code in the // assembly Microsoft.VisualBasic. If it were more frequent, the optional count @@ -1490,7 +1511,7 @@ private static MethodInformation FindBestMethodImpl( } } - OverloadCandidate candidate = new OverloadCandidate(method, arguments.Length); + OverloadCandidate candidate = new OverloadCandidate(methodInfo, arguments.Length); for (int j = 0; candidate != null && j < parameters.Length; j++) { ParameterInformation parameter = parameters[j]; @@ -1590,6 +1611,22 @@ private static MethodInformation FindBestMethodImpl( methods[0].method.DeclaringType.FullName); return null; } + else if (genericParameters.Length != 0 && genericParameters.Contains(null)) + { + errorId = "TypeNotFoundForGenericMethod"; + errorMsg = ExtendedTypeSystem.MethodGenericArgumentTypeNotFoundException; + return null; + } + else if (genericParameters.Length != 0) + { + errorId = "MethodCountCouldNotFindBestGeneric"; + errorMsg = string.Format( + ExtendedTypeSystem.MethodGenericArgumentCountException, + methods[0].method.Name, + genericParameters.Length, + arguments.Length); + return null; + } else { errorId = "MethodCountCouldNotFindBest"; diff --git a/src/System.Management.Automation/engine/MshMemberInfo.cs b/src/System.Management.Automation/engine/MshMemberInfo.cs index 73ee243f6e8..ef6d185a606 100644 --- a/src/System.Management.Automation/engine/MshMemberInfo.cs +++ b/src/System.Management.Automation/engine/MshMemberInfo.cs @@ -1909,9 +1909,18 @@ internal class PSMethodInvocationConstraints internal PSMethodInvocationConstraints( Type methodTargetType, Type[] parameterTypes) + : this(methodTargetType, genericTypeParameters: null, parameterTypes) { - this.MethodTargetType = methodTargetType; + } + + internal PSMethodInvocationConstraints( + Type methodTargetType, + Type[] genericTypeParameters, + Type[] parameterTypes) + { + MethodTargetType = methodTargetType; _parameterTypes = parameterTypes; + GenericTypeParameters = genericTypeParameters; } /// @@ -1926,6 +1935,11 @@ internal PSMethodInvocationConstraints( private readonly Type[] _parameterTypes; + /// + /// Gets the generic type parameters for the method invocation. + /// + public Type[] GenericTypeParameters { get; } + internal static bool EqualsForCollection(ICollection xs, ICollection ys) { if (xs == null) @@ -1946,8 +1960,6 @@ internal static bool EqualsForCollection(ICollection xs, ICollection ys return xs.SequenceEqual(ys); } - // TODO: IEnumerable genericTypeParameters { get; private set; } - public bool Equals(PSMethodInvocationConstraints other) { if (other is null) @@ -1970,6 +1982,11 @@ public bool Equals(PSMethodInvocationConstraints other) return false; } + if (!EqualsForCollection(GenericTypeParameters, other.GenericTypeParameters)) + { + return false; + } + return true; } @@ -1994,18 +2011,7 @@ public override bool Equals(object obj) } public override int GetHashCode() - { - // algorithm based on https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-system-object-gethashcode - unchecked - { - int result = 61; - - result = result * 397 + (MethodTargetType != null ? MethodTargetType.GetHashCode() : 0); - result = result * 397 + ParameterTypes.SequenceGetHashCode(); - - return result; - } - } + => HashCode.Combine(MethodTargetType, ParameterTypes, GenericTypeParameters); public override string ToString() { @@ -2018,6 +2024,22 @@ public override string ToString() separator = " "; } + if (GenericTypeParameters != null) + { + sb.Append(separator); + sb.Append("genericTypeParams: "); + + separator = string.Empty; + foreach (Type parameter in GenericTypeParameters) + { + sb.Append(separator); + sb.Append(ToStringCodeMethods.Type(parameter, dropNamespaces: true)); + separator = ", "; + } + + separator = " "; + } + if (_parameterTypes != null) { sb.Append(separator); @@ -5035,7 +5057,7 @@ internal struct Enumerator : IEnumerator private readonly PSMemberInfoInternalCollection _allMembers; /// - /// Constructs this instance to enumerate over members. + /// Initializes a new instance of the class to enumerate over members. /// /// Members we are enumerating. internal Enumerator(PSMemberInfoIntegratingCollection integratingCollection) @@ -5063,8 +5085,8 @@ internal Enumerator(PSMemberInfoIntegratingCollection integratingCollection) /// Moves to the next element in the enumeration. /// /// - /// false if there are no more elements to enumerate - /// true otherwise + /// If there are no more elements to enumerate, returns false. + /// Returns true otherwise. /// public bool MoveNext() { @@ -5093,7 +5115,7 @@ public bool MoveNext() } /// - /// Current PSMemberInfo in the enumeration. + /// Gets the current PSMemberInfo in the enumeration. /// /// For invalid arguments. T IEnumerator.Current diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 14057f05f68..1178d590a55 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -1172,24 +1172,34 @@ internal static Type GetTypeConstraintForMethodResolution(ExpressionAst expr) return firstConvert?.Type.TypeName.GetReflectionType(); } - internal static PSMethodInvocationConstraints CombineTypeConstraintForMethodResolution(Type targetType, Type argType) + internal static PSMethodInvocationConstraints CombineTypeConstraintForMethodResolution( + Type targetType, + Type argType, + Type[] genericArguments = null) { - if (targetType == null && argType == null) + if (targetType is null + && argType is null + && (genericArguments is null || genericArguments.Length == 0)) { return null; } - return new PSMethodInvocationConstraints(targetType, new[] { argType }); + return new PSMethodInvocationConstraints(targetType, genericArguments, new[] { argType }); } - internal static PSMethodInvocationConstraints CombineTypeConstraintForMethodResolution(Type targetType, Type[] argTypes) + internal static PSMethodInvocationConstraints CombineTypeConstraintForMethodResolution( + Type targetType, + Type[] argTypes, + Type[] genericArguments = null) { - if (targetType == null && (argTypes == null || argTypes.Length == 0)) + if (targetType is null + && (argTypes is null || argTypes.Length == 0) + && (genericArguments is null || genericArguments.Length == 0)) { return null; } - return new PSMethodInvocationConstraints(targetType, argTypes); + return new PSMethodInvocationConstraints(targetType, genericArguments, argTypes); } internal static Expression ConvertValue(TypeConstraintAst typeConstraint, Expression expr) @@ -6310,17 +6320,47 @@ public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) internal static PSMethodInvocationConstraints GetInvokeMemberConstraints(InvokeMemberExpressionAst invokeMemberExpressionAst) { - var arguments = invokeMemberExpressionAst.Arguments; + ReadOnlyCollection arguments = invokeMemberExpressionAst.Arguments; + Type[] argumentTypes = null; + if (arguments is not null) + { + argumentTypes = new Type[arguments.Count]; + for (var i = 0; i < arguments.Count; i++) + { + argumentTypes[i] = GetTypeConstraintForMethodResolution(arguments[i]); + } + } + var targetTypeConstraint = GetTypeConstraintForMethodResolution(invokeMemberExpressionAst.Expression); - return CombineTypeConstraintForMethodResolution( - targetTypeConstraint, - arguments?.Select(Compiler.GetTypeConstraintForMethodResolution).ToArray()); + + ReadOnlyCollection genericArguments = invokeMemberExpressionAst.GenericTypeArguments; + Type[] genericTypeArguments = null; + if (genericArguments is not null) + { + genericTypeArguments = new Type[genericArguments.Count]; + for (var i = 0; i < genericArguments.Count; i++) + { + genericTypeArguments[i] = genericArguments[i].GetReflectionType(); + } + } + + return CombineTypeConstraintForMethodResolution(targetTypeConstraint, argumentTypes, genericTypeArguments); } internal static PSMethodInvocationConstraints GetInvokeMemberConstraints(BaseCtorInvokeMemberExpressionAst invokeMemberExpressionAst) { Type targetTypeConstraint = null; - var arguments = invokeMemberExpressionAst.Arguments; + ReadOnlyCollection arguments = invokeMemberExpressionAst.Arguments; + Type[] argumentTypes = null; + if (arguments is not null) + { + argumentTypes = new Type[arguments.Count]; + for (var i = 0; i < arguments.Count; i++) + { + argumentTypes[i] = GetTypeConstraintForMethodResolution(arguments[i]); + } + } + TypeDefinitionAst typeDefinitionAst = Ast.GetAncestorTypeDefinitionAst(invokeMemberExpressionAst); if (typeDefinitionAst != null) { @@ -6331,9 +6371,7 @@ internal static PSMethodInvocationConstraints GetInvokeMemberConstraints(BaseCto Diagnostics.Assert(false, "BaseCtorInvokeMemberExpressionAst must be used only inside TypeDefinitionAst"); } - return CombineTypeConstraintForMethodResolution( - targetTypeConstraint, - arguments?.Select(Compiler.GetTypeConstraintForMethodResolution).ToArray()); + return CombineTypeConstraintForMethodResolution(targetTypeConstraint, argumentTypes, genericArguments: null); } internal Expression InvokeMember( diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 3afc55e9861..6e7e0ab94ac 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -1357,7 +1357,7 @@ private ITypeName FinishTypeNameRule(Token typeName, bool unBracketedGenericArg case TokenKind.LBracket: case TokenKind.Identifier: - return GenericTypeArgumentsRule(typeName, token, unBracketedGenericArg); + return GenericTypeNameRule(typeName, token, unBracketedGenericArg); default: // ErrorRecovery: sync to ']', and return non-null to avoid cascading errors. @@ -1437,7 +1437,7 @@ private ITypeName GetSingleGenericArgument(Token firstToken) return typeName; } - private ITypeName GenericTypeArgumentsRule(Token genericTypeName, Token firstToken, bool unBracketedGenericArg) + private List GenericTypeArgumentsRule(Token firstToken, out Token lastToken) { Diagnostics.Assert(firstToken.Kind == TokenKind.Identifier || firstToken.Kind == TokenKind.LBracket, "unexpected first token"); RuntimeHelpers.EnsureSufficientExecutionStack(); @@ -1446,20 +1446,18 @@ private ITypeName GenericTypeArgumentsRule(Token genericTypeName, Token firstTok ITypeName typeName = GetSingleGenericArgument(firstToken); genericArguments.Add(typeName); - Token commaOrRBracketToken; - Token token; while (true) { SkipNewlines(); - commaOrRBracketToken = NextToken(); - if (commaOrRBracketToken.Kind != TokenKind.Comma) + lastToken = NextToken(); + if (lastToken.Kind != TokenKind.Comma) { break; } SkipNewlines(); - token = PeekToken(); + Token token = PeekToken(); if (token.Kind == TokenKind.Identifier || token.Kind == TokenKind.LBracket) { SkipToken(); @@ -1467,43 +1465,55 @@ private ITypeName GenericTypeArgumentsRule(Token genericTypeName, Token firstTok } else { - ReportIncompleteInput(After(commaOrRBracketToken), + ReportIncompleteInput( + After(lastToken), nameof(ParserStrings.MissingTypename), ParserStrings.MissingTypename); - typeName = new TypeName(commaOrRBracketToken.Extent, ":ErrorTypeName:"); + typeName = new TypeName(lastToken.Extent, ":ErrorTypeName:"); } genericArguments.Add(typeName); } - if (commaOrRBracketToken.Kind != TokenKind.RBracket) + return genericArguments; + } + + private ITypeName GenericTypeNameRule(Token genericTypeName, Token firstToken, bool unbracketedGenericArg) + { + List genericArguments = GenericTypeArgumentsRule(firstToken, out Token rBracketToken); + + if (rBracketToken.Kind != TokenKind.RBracket) { // ErrorRecovery: pretend we had the closing bracket and just continue on. - - UngetToken(commaOrRBracketToken); - ReportIncompleteInput(Before(commaOrRBracketToken), + UngetToken(rBracketToken); + ReportIncompleteInput( + Before(rBracketToken), nameof(ParserStrings.EndSquareBracketExpectedAtEndOfAttribute), ParserStrings.EndSquareBracketExpectedAtEndOfAttribute); - commaOrRBracketToken = null; + rBracketToken = null; } var openGenericType = new TypeName(genericTypeName.Extent, genericTypeName.Text); - var result = new GenericTypeName(ExtentOf(genericTypeName.Extent, ExtentFromFirstOf(commaOrRBracketToken, genericArguments.LastOrDefault(), firstToken)), - openGenericType, genericArguments); - token = PeekToken(); + var result = new GenericTypeName( + ExtentOf(genericTypeName.Extent, ExtentFromFirstOf(rBracketToken, genericArguments.LastOrDefault(), firstToken)), + openGenericType, + genericArguments); + + Token token = PeekToken(); if (token.Kind == TokenKind.LBracket) { SkipToken(); return CompleteArrayTypeName(result, openGenericType, NextToken()); } - if (token.Kind == TokenKind.Comma && !unBracketedGenericArg) + if (token.Kind == TokenKind.Comma && !unbracketedGenericArg) { SkipToken(); string assemblyNameSpec = _tokenizer.GetAssemblyNameSpec(); if (string.IsNullOrEmpty(assemblyNameSpec)) { - ReportError(After(token), + ReportError( + After(token), nameof(ParserStrings.MissingAssemblyNameSpecification), ParserStrings.MissingAssemblyNameSpecification); } @@ -7703,6 +7713,7 @@ private ExpressionAst MemberAccessRule(ExpressionAst targetExpr, Token operatorT // On entry, we've verified that operatorToken is not preceded by whitespace. CommandElementAst member = MemberNameRule(); + List genericTypeArguments = null; if (member == null) { @@ -7715,34 +7726,106 @@ private ExpressionAst MemberAccessRule(ExpressionAst targetExpr, Token operatorT member = GetSingleCommandArgument(CommandArgumentContext.CommandArgument) ?? new ErrorExpressionAst(ExtentOf(targetExpr, operatorToken)); } - else + else if (_ungotToken == null) { + // Member name may be an incomplete token like `$a.$(Command-Name`; we do not look for generic args or + // invocation token(s) if the member name token is recognisably incomplete. + genericTypeArguments = GenericMethodArgumentsRule(out Token rBracket); Token lParen = NextInvokeMemberToken(); + if (lParen != null) { + // When we reach here, we either had a legit section of generic arguments (in which case, `rBracket` + // won't be null), or we saw `lParen` directly following the member token (in which case, `rBracket` + // will be null). + int endColumnNumber = rBracket is null ? member.Extent.EndColumnNumber : rBracket.Extent.EndColumnNumber; + Diagnostics.Assert(lParen.Kind == TokenKind.LParen || lParen.Kind == TokenKind.LCurly, "token kind incorrect"); - Diagnostics.Assert(member.Extent.EndColumnNumber == lParen.Extent.StartColumnNumber, - "member and paren must be adjacent"); - return MemberInvokeRule(targetExpr, lParen, operatorToken, member); + Diagnostics.Assert( + endColumnNumber == lParen.Extent.StartColumnNumber, + "member and paren must be adjacent when the method is not generic"); + return MemberInvokeRule(targetExpr, lParen, operatorToken, member, genericTypeArguments); } } return new MemberExpressionAst( - ExtentOf(targetExpr, member), - targetExpr, - member, - @static: operatorToken.Kind == TokenKind.ColonColon, - nullConditional: operatorToken.Kind == TokenKind.QuestionDot); + ExtentOf(targetExpr, member), + targetExpr, + member, + @static: operatorToken.Kind == TokenKind.ColonColon, + nullConditional: operatorToken.Kind == TokenKind.QuestionDot, + genericTypeArguments); + } + + private List GenericMethodArgumentsRule(out Token rBracketToken) + { + List genericTypes = null; + + int resyncIndex = _tokenizer.GetRestorePoint(); + Token lBracket = NextToken(); + rBracketToken = null; + + if (lBracket.Kind != TokenKind.LBracket) + { + // We cannot avoid this Resync(); if we use PeekToken() to try to avoid a Resync(), the method called + // after this [`NextInvokeMemberToken()` or `NextMemberAccessToken()`] will note that an _ungotToken + // is present and assume an error state. That will cause any property accesses or non-generic method + // calls to throw a parse error. + Resync(resyncIndex); + return null; + } + + // This is either a InvokeMember expression with generic type arguments, or some sort of collection index + // on a property. + TokenizerMode oldTokenizerMode = _tokenizer.Mode; + try + { + // Switch to typename mode to avoid aggressive argument tokenization. + SetTokenizerMode(TokenizerMode.TypeName); + + SkipNewlines(); + Token firstToken = NextToken(); + if (firstToken.Kind == TokenKind.Identifier || firstToken.Kind == TokenKind.LBracket) + { + resyncIndex = -1; + genericTypes = GenericTypeArgumentsRule(firstToken, out rBracketToken); + + if (rBracketToken.Kind != TokenKind.RBracket) + { + UngetToken(rBracketToken); + ReportIncompleteInput( + Before(rBracketToken), + nameof(ParserStrings.EndSquareBracketExpectedAtEndOfType), + ParserStrings.EndSquareBracketExpectedAtEndOfType); + rBracketToken = null; + } + } + } + finally + { + SetTokenizerMode(oldTokenizerMode); + } + + if (resyncIndex > 0) + { + Resync(resyncIndex); + } + + return genericTypes; } - private ExpressionAst MemberInvokeRule(ExpressionAst targetExpr, Token lBracket, Token operatorToken, CommandElementAst member) + private ExpressionAst MemberInvokeRule( + ExpressionAst targetExpr, + Token lBracket, + Token operatorToken, + CommandElementAst member, + IList genericTypes) { // G invocation-expression: target-expression passed as a parameter. lBracket can be '(' or '{'. // G target-expression member-name invoke-param-list // G invoke-param-list: // G '(' invoke-param-paren-list // G script-block - IScriptExtent lastExtent = null; List arguments; @@ -7753,6 +7836,7 @@ private ExpressionAst MemberInvokeRule(ExpressionAst targetExpr, Token lBracket, else { arguments = new List(); + // handle the construct $x.methodName{2+2} as through it had been written $x.methodName({2+2}) SkipNewlines(); ExpressionAst argument = ScriptBlockExpressionRule(lBracket); @@ -7766,7 +7850,8 @@ private ExpressionAst MemberInvokeRule(ExpressionAst targetExpr, Token lBracket, member, arguments, operatorToken.Kind == TokenKind.ColonColon, - operatorToken.Kind == TokenKind.QuestionDot); + operatorToken.Kind == TokenKind.QuestionDot, + genericTypes); } private List InvokeParamParenListRule(Token lParen, out IScriptExtent lastExtent) diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 3e1f9ce60a5..2d078581656 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -7862,7 +7862,7 @@ internal bool IsRef() public class MemberExpressionAst : ExpressionAst, ISupportsAssignment { /// - /// Construct an ast to reference a property. + /// Initializes a new instance of the class. /// /// /// The extent of the expression, starting with the expression before the operator '.' or '::' and ending after @@ -7873,10 +7873,16 @@ public class MemberExpressionAst : ExpressionAst, ISupportsAssignment /// True if the '::' operator was used, false if '.' is used. /// True if the member access is for a static member, using '::', false if accessing a member on an instance using '.'. /// + /// The generic type arguments passed to the member. /// /// If , , or is null. /// - public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst member, bool @static) + public MemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst member, + bool @static, + IList genericTypes) : base(extent) { if (expression == null || member == null) @@ -7889,6 +7895,35 @@ public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, Comma this.Member = member; SetParent(member); this.Static = @static; + + if (genericTypes is not null && genericTypes.Count > 0) + { + this.GenericTypeArguments = new ReadOnlyCollection(genericTypes); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The extent of the expression, starting with the expression before the operator '.' or '::' and ending after + /// membername or expression naming the member. + /// + /// The expression before the member access operator '.' or '::'. + /// The name or expression naming the member to access. + /// True if the '::' operator was used, false if '.' is used. + /// True if the member access is for a static member, using '::', false if accessing a member on an instance using '.'. + /// + /// + /// If , , or is null. + /// + public MemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst member, + bool @static) + : this(extent, expression, member, @static, genericTypes: null) + { } /// @@ -7902,15 +7937,46 @@ public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, Comma /// The name or expression naming the member to access. /// True if the '::' operator was used, false if '.' or '?.' is used. /// True if '?.' used. + /// The generic type arguments passed to the member. /// /// If , , or is null. /// - public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst member, bool @static, bool nullConditional) - : this(extent, expression, member, @static) + public MemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst member, + bool @static, + bool nullConditional, + IList genericTypes) + : this(extent, expression, member, @static, genericTypes) { this.NullConditional = nullConditional; } + /// + /// Initializes a new instance of the class. + /// + /// + /// The extent of the expression, starting with the expression before the operator '.', '::' or '?.' and ending after + /// membername or expression naming the member. + /// + /// The expression before the member access operator '.', '::' or '?.'. + /// The name or expression naming the member to access. + /// True if the '::' operator was used, false if '.' or '?.' is used. + /// True if '?.' used. + /// + /// If , , or is null. + /// + public MemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst member, + bool @static, + bool nullConditional) + : this(extent, expression, member, @static, nullConditional, genericTypes: null) + { + } + /// /// The expression that produces the value to retrieve the member from. This property is never null. /// @@ -7931,6 +7997,11 @@ public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, Comma /// public bool NullConditional { get; protected set; } + /// + /// Gets a list of generic type arguments passed to this member. + /// + public ReadOnlyCollection GenericTypeArguments { get; } + /// /// Copy the MemberExpressionAst instance. /// @@ -7938,7 +8009,14 @@ public override Ast Copy() { var newExpression = CopyElement(this.Expression); var newMember = CopyElement(this.Member); - return new MemberExpressionAst(this.Extent, newExpression, newMember, this.Static, this.NullConditional); + + return new MemberExpressionAst( + this.Extent, + newExpression, + newMember, + this.Static, + this.NullConditional, + this.GenericTypeArguments); } #region Visitors @@ -7974,7 +8052,7 @@ IAssignableValue ISupportsAssignment.GetAssignableValue() public class InvokeMemberExpressionAst : MemberExpressionAst, ISupportsAssignment { /// - /// Construct an instance of a method invocation expression. + /// Initializes a new instance of the class. /// /// /// The extent of the expression, starting with the expression before the invocation operator and ending with the @@ -7986,11 +8064,18 @@ public class InvokeMemberExpressionAst : MemberExpressionAst, ISupportsAssignmen /// /// True if the invocation is for a static method, using '::', false if invoking a method on an instance using '.'. /// + /// The generic type arguments passed to the method. /// /// If is null. /// - public InvokeMemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst method, IEnumerable arguments, bool @static) - : base(extent, expression, method, @static) + public InvokeMemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst method, + IEnumerable arguments, + bool @static, + IList genericTypes) + : base(extent, expression, method, @static, genericTypes) { if (arguments != null && arguments.Any()) { @@ -7999,6 +8084,32 @@ public InvokeMemberExpressionAst(IScriptExtent extent, ExpressionAst expression, } } + /// + /// Initializes a new instance of the class. + /// + /// + /// The extent of the expression, starting with the expression before the invocation operator and ending with the + /// closing paren after the arguments. + /// + /// The expression before the invocation operator ('.', '::'). + /// The method to invoke. + /// The arguments to pass to the method. + /// + /// True if the invocation is for a static method, using '::', false if invoking a method on an instance using '.'. + /// + /// + /// If is null. + /// + public InvokeMemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst method, + IEnumerable arguments, + bool @static) + : this(extent, expression, method, arguments, @static, genericTypes: null) + { + } + /// /// Initializes a new instance of the class. /// @@ -8013,15 +8124,51 @@ public InvokeMemberExpressionAst(IScriptExtent extent, ExpressionAst expression, /// True if the invocation is for a static method, using '::', false if invoking a method on an instance using '.' or '?.'. /// /// True if the operator used is '?.'. + /// The generic type arguments passed to the method. /// /// If is null. /// - public InvokeMemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst method, IEnumerable arguments, bool @static, bool nullConditional) - : this(extent, expression, method, arguments, @static) + public InvokeMemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst method, + IEnumerable arguments, + bool @static, + bool nullConditional, + IList genericTypes) + : this(extent, expression, method, arguments, @static, genericTypes) { this.NullConditional = nullConditional; } + /// + /// Initializes a new instance of the class. + /// + /// + /// The extent of the expression, starting with the expression before the invocation operator and ending with the + /// closing paren after the arguments. + /// + /// The expression before the invocation operator ('.', '::' or '?.'). + /// The method to invoke. + /// The arguments to pass to the method. + /// + /// True if the invocation is for a static method, using '::', false if invoking a method on an instance using '.' or '?.'. + /// + /// True if the operator used is '?.'. + /// + /// If is null. + /// + public InvokeMemberExpressionAst( + IScriptExtent extent, + ExpressionAst expression, + CommandElementAst method, + IEnumerable arguments, + bool @static, + bool nullConditional) + : this(extent, expression, method, arguments, @static, nullConditional, genericTypes: null) + { + } + /// /// The non-empty collection of arguments to pass when invoking the method, or null if no arguments were specified. /// @@ -8035,7 +8182,15 @@ public override Ast Copy() var newExpression = CopyElement(this.Expression); var newMethod = CopyElement(this.Member); var newArguments = CopyElements(this.Arguments); - return new InvokeMemberExpressionAst(this.Extent, newExpression, newMethod, newArguments, this.Static, this.NullConditional); + + return new InvokeMemberExpressionAst( + this.Extent, + newExpression, + newMethod, + newArguments, + this.Static, + this.NullConditional, + this.GenericTypeArguments); } #region Visitors diff --git a/src/System.Management.Automation/resources/ExtendedTypeSystem.resx b/src/System.Management.Automation/resources/ExtendedTypeSystem.resx index 6a697cba888..d617cbf7a38 100644 --- a/src/System.Management.Automation/resources/ExtendedTypeSystem.resx +++ b/src/System.Management.Automation/resources/ExtendedTypeSystem.resx @@ -156,6 +156,12 @@ Cannot find an overload for "{0}" and the argument count: "{1}". + + Could not find a suitable generic method overload for "{0}" with "{1}" type parameters, and the argument count: "{2}". + + + One or more of the generic type parameters provided for the method "{0}" refers to a type which cannot be found. + Multiple ambiguous overloads found for "{0}" and the argument count: "{1}". diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 39ab4968b5e..14ebc5a502e 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -43,6 +43,22 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches[0].CompletionText | Should -BeExactly 'CompareTo(' } + It 'should complete generic type parameters for static methods' { + $script = '[array]::Empty[pscu' + + $results = TabExpansion2 -inputScript $script -cursorColumn $script.Length + $results.CompletionMatches.CompletionText | Should -Contain 'pscustomobject' + } + + It 'should complete generic type parameters for instance methods' { + $script = ' + $dict = [System.Collections.Concurrent.ConcurrentDictionary[string, int]]::new() + $dict.AddOrUpdate[pscu' + + $results = TabExpansion2 -inputScript $script -cursorColumn $script.Length + $results.CompletionMatches.CompletionText | Should -Contain 'pscustomobject' + } + It 'Should complete Magic foreach' { $res = TabExpansion2 -inputScript '(1..10).Fo' -cursorColumn '(1..10).Fo'.Length $res.CompletionMatches[0].CompletionText | Should -BeExactly 'ForEach(' diff --git a/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 b/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 index f28b6801389..e6a91891174 100644 --- a/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 +++ b/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 @@ -81,3 +81,196 @@ namespace MSFT_716893 ([MSFT_716893.IInterface1]$proxy).BaseOperation(22) | Should -Be "3 - 22" } } + +Describe 'Generic Method invocation' -Tags 'CI' { + + BeforeAll { + $EmptyArrayCases = @( + @{ + Script = '[Array]::Empty[string]()' + ExpectedType = [string[]] + } + @{ + Script = '[Array]::Empty[System.Collections.Generic.Dictionary[System.Numerics.BigInteger, System.Collections.Generic.List[string[,]]]]()' + ExpectedType = [System.Collections.Generic.Dictionary[System.Numerics.BigInteger, System.Collections.Generic.List[string[, ]]][]] + } + @{ + Script = '[Array]::$("Empty")[[System.Collections.Generic.Dictionary[[System.String, System.Private.CoreLib],[System.Numerics.BigInteger, System.Runtime.Numerics]], System.Private.CoreLib]]()' + ExpectedType = [System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib], [System.Numerics.BigInteger, System.Runtime.Numerics]][], System.Private.CoreLib] + } + ) + + $ExpectedParseErrors = @( + @{ + Script = '$object.Method[incompl' + ExpectedErrors = 'EndSquareBracketExpectedAtEndOfType' + ErrorCount = 1 + } + @{ + Script = '[type]::Member[incompl' + ExpectedErrors = 'EndSquareBracketExpectedAtEndOfType' + ErrorCount = 1 + } + @{ + Script = '$object.Method[Type1[Type2' + ExpectedErrors = 'UnexpectedToken','EndSquareBracketExpectedAtEndOfType' + ErrorCount = 2 + } + @{ + Script = '[array]::empty[type]]()' + ExpectedErrors = 'UnexpectedToken', 'ExpectedExpression' + ErrorCount = 2 + } + @{ + Script = '$object.Method[type,]()' + ExpectedErrors = 'MissingTypeName' + ErrorCount = 1 + } + @{ + Script = '$object.Method[]()' + ExpectedErrors = 'MissingArrayIndexExpression', 'UnexpectedToken', 'ExpectedExpression' + ErrorCount = 3 + } + @{ + Script = '$object.Method[,]()' + ExpectedErrors = 'MissingExpressionAfterOperator', 'UnexpectedToken', 'ExprectedExpression' + ErrorCount = 3 + } + @{ + Script = '$object.Method[,type]()' + ExpectedErrors = 'MissingExpressionAfterOperator', 'UnexpectedToken', 'ExpectedExpression' + ErrorCount = 3 + } + @{ + Script = '$object.Method[type()' + ExpectedErrors = 'UnexpectedToken', 'ExpectedExpression' + ErrorCount = 2 + } + @{ + Script = '$object.Method[type)' + ExpectedErrors = 'EndSquareBracketExpectedAtEndOfType', 'UnexpectedToken' + ErrorCount = 2 + } + ) + } + + It 'does not throw a parse error for "