diff --git a/ICSharpCode.NRefactory.Tests/CSharp/Parser/TypeMembers/MethodDeclarationTests.cs b/ICSharpCode.NRefactory.Tests/CSharp/Parser/TypeMembers/MethodDeclarationTests.cs index cdb39e3d09..53e0f31388 100644 --- a/ICSharpCode.NRefactory.Tests/CSharp/Parser/TypeMembers/MethodDeclarationTests.cs +++ b/ICSharpCode.NRefactory.Tests/CSharp/Parser/TypeMembers/MethodDeclarationTests.cs @@ -250,6 +250,7 @@ namespace ICSharpCode.NRefactory.CSharp.Parser.TypeMembers Assert.AreEqual("s", md.Parameters[0].ParameterName); Assert.AreEqual("System.String", md.Parameters[0].TypeReference.Type); } + */ [Test] public void VoidExtensionMethodTest() @@ -258,11 +259,13 @@ namespace ICSharpCode.NRefactory.CSharp.Parser.TypeMembers "public static void Print(this string s) { Console.WriteLine(s); }" ); Assert.AreEqual("Print", md.Name); + Assert.AreEqual("s", md.Parameters.First().Name); + Assert.AreEqual(ParameterModifier.This, md.Parameters.First().ParameterModifier); + Assert.AreEqual("string", ((PrimitiveType)md.Parameters.First().Type).Keyword); Assert.IsTrue(md.IsExtensionMethod); - Assert.AreEqual("s", md.Parameters[0].ParameterName); - Assert.AreEqual("System.String", md.Parameters[0].TypeReference.Type); } + /* TODO [Test] public void MethodWithEmptyAssignmentErrorInBody() { diff --git a/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExtensionMethodTests.cs b/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExtensionMethodTests.cs index 7c30ceee70..00e531387d 100644 --- a/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExtensionMethodTests.cs +++ b/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExtensionMethodTests.cs @@ -7,7 +7,7 @@ using NUnit.Framework; namespace ICSharpCode.NRefactory.CSharp.Resolver { - [TestFixture, Ignore("Extension method support not yet implemented")] + [TestFixture] public class ExtensionMethodTests : ResolverTestBase { [Test] diff --git a/ICSharpCode.NRefactory/CSharp/Parser/CSharpParser.cs b/ICSharpCode.NRefactory/CSharp/Parser/CSharpParser.cs index 38dce34549..1b04ed7f1b 100644 --- a/ICSharpCode.NRefactory/CSharp/Parser/CSharpParser.cs +++ b/ICSharpCode.NRefactory/CSharp/Parser/CSharpParser.cs @@ -1762,10 +1762,12 @@ namespace ICSharpCode.NRefactory.CSharp if (location != null) parameterDeclarationExpression.AddChild (new CSharpTokenNode (Convert (location[0]), "params".Length), ParameterDeclaration.Roles.Keyword); break; - case Parameter.Modifier.This: - parameterDeclarationExpression.ParameterModifier = ParameterModifier.This; - if (location != null) - parameterDeclarationExpression.AddChild (new CSharpTokenNode (Convert (location[0]), "this".Length), ParameterDeclaration.Roles.Keyword); + default: + if (p.HasExtensionMethodModifier) { + parameterDeclarationExpression.ParameterModifier = ParameterModifier.This; + if (location != null) + parameterDeclarationExpression.AddChild (new CSharpTokenNode (Convert (location[0]), "this".Length), ParameterDeclaration.Roles.Keyword); + } break; } if (p.TypeExpression != null) // lambdas may have no types (a, b) => ... diff --git a/ICSharpCode.NRefactory/CSharp/Resolver/CSharpResolver.cs b/ICSharpCode.NRefactory/CSharp/Resolver/CSharpResolver.cs index 400597ad87..a47a5acb91 100644 --- a/ICSharpCode.NRefactory/CSharp/Resolver/CSharpResolver.cs +++ b/ICSharpCode.NRefactory/CSharp/Resolver/CSharpResolver.cs @@ -1688,7 +1688,16 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver return DynamicResult; MemberLookup lookup = CreateMemberLookup(); - return lookup.Lookup(target.Type, identifier, typeArguments, isInvocationTarget); + ResolveResult result = lookup.Lookup(target.Type, identifier, typeArguments, isInvocationTarget); + if (result is UnknownMemberResolveResult) { + var extensionMethods = GetExtensionMethods(target.Type, identifier, typeArguments.Count); + if (extensionMethods.Count > 0) { + return new MethodGroupResolveResult(target.Type, identifier, EmptyList.Instance, typeArguments) { + ExtensionMethods = extensionMethods + }; + } + } + return result; } MemberLookup CreateMemberLookup() @@ -1697,6 +1706,61 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver } #endregion + #region GetExtensionMethods + /// + /// Gets the extension methods that are called 'name', and can be called with 'typeArgumentCount' explicit type arguments; + /// and are applicable with a first argument type of 'targetType'. + /// + List> GetExtensionMethods(IType targetType, string name, int typeArgumentCount) + { + List> extensionMethodGroups = new List>(); + foreach (var inputGroup in GetAllExtensionMethods()) { + List outputGroup = new List(); + foreach (var method in inputGroup) { + if (method.Name == name && (typeArgumentCount == 0 || method.TypeParameters.Count == typeArgumentCount)) { + // TODO: verify targetType + outputGroup.Add(method); + } + } + if (outputGroup.Count > 0) + extensionMethodGroups.Add(outputGroup); + } + return extensionMethodGroups; + } + + List> GetAllExtensionMethods() + { + // TODO: maybe cache the result? + List> extensionMethodGroups = new List>(); + List m; + for (UsingScope scope = this.UsingScope; scope != null; scope = scope.Parent) { + m = GetExtensionMethods(scope.NamespaceName).ToList(); + if (m.Count > 0) + extensionMethodGroups.Add(m); + + m = ( + from u in scope.Usings + select u.ResolveNamespace(context) into ns + where ns != null + select ns.NamespaceName + ).Distinct().SelectMany(ns => GetExtensionMethods(ns)).ToList(); + if (m.Count > 0) + extensionMethodGroups.Add(m); + } + return extensionMethodGroups; + } + + IEnumerable GetExtensionMethods(string namespaceName) + { + return + from c in context.GetClasses(namespaceName, StringComparer.Ordinal) + where c.IsStatic + from m in c.Methods + where (m.IsExtensionMethod) + select m; + } + #endregion + #region ResolveInvocation public ResolveResult ResolveInvocation(ResolveResult target, ResolveResult[] arguments, string[] argumentNames = null) { @@ -1709,15 +1773,57 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver MethodGroupResolveResult mgrr = target as MethodGroupResolveResult; if (mgrr != null) { - OverloadResolution or = new OverloadResolution(context, arguments, argumentNames, mgrr.TypeArguments.ToArray()); + var typeArgumentArray = mgrr.TypeArguments.ToArray(); + OverloadResolution or = new OverloadResolution(context, arguments, argumentNames, typeArgumentArray); foreach (IMethod method in mgrr.Methods) { // TODO: grouping by class definition? or.AddCandidate(method); } - // TODO: extension methods? - IType returnType = or.BestCandidate.ReturnType.Resolve(context); - returnType = returnType.AcceptVisitor(new MethodTypeParameterSubstitution(or.InferredTypeArguments)); - return new MemberResolveResult(or.BestCandidate, returnType); + if (!or.FoundApplicableCandidate) { + // No applicable match found, so let's try extension methods. + + var extensionMethods = mgrr.ExtensionMethods; + // Look in extension methods pre-calcalculated by ResolveMemberAccess if possible; + // otherwise call GetExtensionMethods(). + if (extensionMethods == null) + extensionMethods = GetExtensionMethods(mgrr.TargetType, mgrr.MethodName, mgrr.TypeArguments.Count); + + if (extensionMethods.Count > 0) { + ResolveResult[] extArguments = new ResolveResult[arguments.Length + 1]; + extArguments[0] = new ResolveResult(mgrr.TargetType); + arguments.CopyTo(extArguments, 1); + string[] extArgumentNames = null; + if (argumentNames != null) { + extArgumentNames = new string[argumentNames.Length + 1]; + argumentNames.CopyTo(extArgumentNames, 1); + } + var extOr = new OverloadResolution(context, extArguments, extArgumentNames, typeArgumentArray); + + foreach (var g in extensionMethods) { + foreach (var m in g) { + extOr.AddCandidate(m); + } + if (extOr.FoundApplicableCandidate) + break; + } + // For the lack of a better comparison function (the one within OverloadResolution + // cannot be used as it depends on the argument set): + if (extOr.FoundApplicableCandidate || or.BestCandidate == null) { + // Consider an extension method result better than the normal result only + // if it's applicable; or if there is no normal result. + or = extOr; + } + } + } + if (or.BestCandidate != null) { + IType returnType = or.BestCandidate.ReturnType.Resolve(context); + returnType = returnType.AcceptVisitor(new MethodTypeParameterSubstitution(or.InferredTypeArguments)); + return new MemberResolveResult(or.BestCandidate, returnType); + } else { + // No candidate found at all (not even an inapplicable one). + // This can happen with empty method groups (as sometimes used with extension methods) + return new UnknownMethodResolveResult(mgrr.TargetType, mgrr.MethodName, mgrr.TypeArguments, CreateParameters(arguments, argumentNames)); + } } UnknownMemberResolveResult umrr = target as UnknownMemberResolveResult; if (umrr != null) { @@ -1826,7 +1932,7 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver OverloadResolution or = new OverloadResolution(context, arguments, argumentNames, new IType[0]); MemberLookup lookup = CreateMemberLookup(); - bool allowProtectedAccess = lookup.AllowProtectedAccess(target.Type); + bool allowProtectedAccess = lookup.IsProtectedAccessAllowed(target.Type); var indexers = target.Type.GetProperties(context, p => p.IsIndexer && lookup.IsAccessible(p, allowProtectedAccess)); // TODO: filter indexers hiding other indexers? foreach (IProperty p in indexers) { @@ -1848,7 +1954,7 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver OverloadResolution or = new OverloadResolution(context, arguments, argumentNames, new IType[0]); MemberLookup lookup = CreateMemberLookup(); - bool allowProtectedAccess = lookup.AllowProtectedAccess(type); + bool allowProtectedAccess = lookup.IsProtectedAccessAllowed(type); var constructors = type.GetConstructors(context, m => lookup.IsAccessible(m, allowProtectedAccess)); foreach (IMethod ctor in constructors) { or.AddCandidate(ctor); diff --git a/ICSharpCode.NRefactory/CSharp/Resolver/MemberLookup.cs b/ICSharpCode.NRefactory/CSharp/Resolver/MemberLookup.cs index aa81779be0..615c23947c 100644 --- a/ICSharpCode.NRefactory/CSharp/Resolver/MemberLookup.cs +++ b/ICSharpCode.NRefactory/CSharp/Resolver/MemberLookup.cs @@ -45,7 +45,7 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver } #region IsAccessible - public bool AllowProtectedAccess(IType targetType) + public bool IsProtectedAccessAllowed(IType targetType) { ITypeDefinition typeDef = targetType.GetDefinition(); return typeDef != null && typeDef.IsDerivedFrom(currentTypeDefinition, context); @@ -164,7 +164,7 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver types.AddRange(type.GetNestedTypes(context, typeFilter)); } - bool allowProtectedAccess = AllowProtectedAccess(type); + bool allowProtectedAccess = IsProtectedAccessAllowed(type); if (typeArgumentCount == 0) { Predicate memberFilter = delegate(IMember member) { @@ -248,7 +248,7 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver if (members.Count == 1 && firstNonMethod != null) return new MemberResolveResult(firstNonMethod, context); if (firstNonMethod == null) - return new MethodGroupResolveResult(members.ConvertAll(m => (IMethod)m), typeArguments); + return new MethodGroupResolveResult(type, name, members.ConvertAll(m => (IMethod)m), typeArguments); return new AmbiguousMemberResultResult(firstNonMethod, firstNonMethod.ReturnType.Resolve(context)); } diff --git a/ICSharpCode.NRefactory/CSharp/Resolver/MethodGroupResolveResult.cs b/ICSharpCode.NRefactory/CSharp/Resolver/MethodGroupResolveResult.cs index 9c5b9d9e57..ec456a013e 100644 --- a/ICSharpCode.NRefactory/CSharp/Resolver/MethodGroupResolveResult.cs +++ b/ICSharpCode.NRefactory/CSharp/Resolver/MethodGroupResolveResult.cs @@ -16,13 +16,33 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver { readonly ReadOnlyCollection methods; readonly ReadOnlyCollection typeArguments; + readonly IType targetType; + readonly string methodName; - public MethodGroupResolveResult(IList methods, IList typeArguments) : base(SharedTypes.UnknownType) + /// + /// List of extension methods, used to avoid re-calculating it in ResolveInvocation() when it was already + /// calculated by ResolveMemberAccess(). + /// + internal List> ExtensionMethods; + + public MethodGroupResolveResult(IType targetType, string methodName, IList methods, IList typeArguments) : base(SharedTypes.UnknownType) { + if (targetType == null) + throw new ArgumentNullException("targetType"); if (methods == null) throw new ArgumentNullException("methods"); + this.targetType = targetType; + this.methodName = methodName; this.methods = new ReadOnlyCollection(methods); - this.typeArguments = new ReadOnlyCollection(typeArguments); + this.typeArguments = typeArguments != null ? new ReadOnlyCollection(typeArguments) : EmptyList.Instance; + } + + public IType TargetType { + get { return targetType; } + } + + public string MethodName { + get { return methodName; } } public ReadOnlyCollection Methods { diff --git a/ICSharpCode.NRefactory/CSharp/Resolver/OverloadResolution.cs b/ICSharpCode.NRefactory/CSharp/Resolver/OverloadResolution.cs index 3ad38ec7d0..6807cd526f 100644 --- a/ICSharpCode.NRefactory/CSharp/Resolver/OverloadResolution.cs +++ b/ICSharpCode.NRefactory/CSharp/Resolver/OverloadResolution.cs @@ -522,6 +522,10 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver } } + public bool FoundApplicableCandidate { + get { return bestCandidate != null && bestCandidate.Errors == OverloadResolutionErrors.None; } + } + public IParameterizedMember BestCandidateAmbiguousWith { get { return bestCandidateAmbiguousWith != null ? bestCandidateAmbiguousWith.Member : null; } }