icsharpcode / NRefactory

NRefactory - Refactoring Your C# Code
684 stars 262 forks source link

Resolving members with CSHarpTypeResolveContext #507

Open W3L-Sebastian-Heimfarth opened 8 years ago

W3L-Sebastian-Heimfarth commented 8 years ago

Hi!

We are using NRefactory for c# code completion and most times it works great :) Recently we found a problem which we think is caused by not finding the correct CSHarpTypeResolveContext. For a given code snippet and a given cursorPosition we calculate the CSHarpTypeResolveContext as follows (this is a simplified version of our production code):

public class TypeResolveContextDetermination
    {
        /// <summary>
        /// Resolves TypeResolveContext like this:
        /// (single) main-assembly -> current usingScope -> innermostTypeDefinition -> currentMember (e.g. Method, Ctor, etc.)
        /// </summary>
        /// <param name="sourceCode"></param>
        /// <param name="cursorIndex"></param>
        /// <returns></returns>
        public CSharpTypeResolveContext DetermineTypeResolveContext(string sourceCode, int cursorIndex)
        {
            var document = new ReadOnlyDocument(sourceCode);
            IProjectContent projectContent;
            CSharpUnresolvedFile unresolvedFile;

            var compilation = CreateCompilation(sourceCode, out projectContent, out unresolvedFile);
            var cursorLocation = cursorIndex > 0 ? document.GetLocation(cursorIndex) : new TextLocation(1, 1);

            var typeResolveContext = new CSharpTypeResolveContext(compilation.MainAssembly);
            typeResolveContext = typeResolveContext.WithUsingScope(unresolvedFile.GetUsingScope(cursorLocation).Resolve(compilation));

            var currentTypeDefinition = unresolvedFile.GetInnermostTypeDefinition(cursorLocation);
            if (currentTypeDefinition != null)
            {
                var resolvedDef = currentTypeDefinition.Resolve(typeResolveContext).GetDefinition();
                typeResolveContext = typeResolveContext.WithCurrentTypeDefinition(resolvedDef);
                var curMember =
                    resolvedDef.Members.FirstOrDefault(m => m.Region.Begin <= cursorLocation && cursorLocation < m.BodyRegion.End);
                if (curMember != null) // for testcase CodeB curMember is null, despite resolvedDef.Members contains MethodA (see tests at the bottom)
                {
                    typeResolveContext = typeResolveContext.WithCurrentMember(curMember);
                }
            }

            return typeResolveContext;
        }

        private ICompilation CreateCompilation(string sourceCode, out IProjectContent projectContent, out CSharpUnresolvedFile unresolvedFile)
        {
            projectContent = new CSharpProjectContent();
            projectContent = AddFileToProjectContent(projectContent, sourceCode, "FileOfInterest.cs", out unresolvedFile);

            projectContent.AddAssemblyReferences(new DefaultAssemblyReference(typeof(List<>).Assembly.GetName().Name));

            var compilation = projectContent.CreateCompilation();
            return compilation;
        }

        private IProjectContent AddFileToProjectContent(
            IProjectContent projectContent,
            string sourceCode,
            string fileName,
            out CSharpUnresolvedFile unresolvedFile)
        {
            SyntaxTree syntaxTree = new CSharpParser().Parse(sourceCode, fileName);
            syntaxTree.Freeze();

            unresolvedFile = syntaxTree.ToTypeSystem();
            return projectContent.AddOrUpdateFiles(unresolvedFile);
        }
    }

We think, the code above should satisfy the following four test cases. But for code snippet CodeB we can not determine the correct context.

namespace NRefactoryTests
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using ICSharpCode.NRefactory;
    using ICSharpCode.NRefactory.CSharp;
    using ICSharpCode.NRefactory.CSharp.TypeSystem;
    using ICSharpCode.NRefactory.Editor;
    using ICSharpCode.NRefactory.TypeSystem;
    using ICSharpCode.NRefactory.TypeSystem.Implementation;
    using NUnit.Framework;

    public class TypeContextTest
    {
        private TypeResolveContextDetermination typeResolveContextDetermination;

        private const string CodeA = 
@"namespace TestNamespace
{
               public class TestClass 
                {
                              public virtual void MethodA()
                              {
                                              TestClass test = new TestClass();
                                               this.$$$
                                               TestClass test2 = new TestClass();
                              }

                               public virtual void MethodB()
                              {

                               }
               }
}";

        private const string CodeB =
@"namespace TestNamespace
{
               public class TestClass 
                {
                              public virtual void MethodA()
                              {
                                              TestClass test = new TestClass();
                                               this.M$$$
                                               TestClass test2 = new TestClass();
                              }

                               public virtual void MethodB()
                              {

                               }
               }
}";

        private const string CodeC =
@"namespace TestNamespace
{
               public class TestClass 
                {
                              public virtual void MethodA()
                              {
                                              TestClass test = new TestClass();
                                               this.M$$$
                              }

                               public virtual void MethodB()
                              {

                               }
               }
}";

        private const string CodeD =
@"namespace TestNamespace
{
               public class TestClass 
                {
                              public virtual void MethodA()
                              {
                                               this.M$$$
                                               TestClass test2 = new TestClass();
                              }

                               public virtual void MethodB()
                              {

                               }
               }
}";

        [SetUp]
        public void Setup()
        {
            typeResolveContextDetermination = new TypeResolveContextDetermination();
        }

        [Test]
        public void ResolveTypeContextWithNRefactory_ContextShouldBeMethodA(
            [Values(CodeA, CodeB, CodeC, CodeD)] string code)
        {
            int cursorPosition = code.IndexOf("$$$", StringComparison.Ordinal);
            code = code.Replace("$$$", string.Empty);

            var context = typeResolveContextDetermination.DetermineTypeResolveContext(code, cursorPosition);

            Assert.NotNull(context);
            Assert.NotNull(context.CurrentMember, "CurrentMember of the context should be MethodA");
            Assert.AreEqual("TestNamespace.TestClass.MethodA", context.CurrentMember.FullName, "CurrentMember of the context should be MethodA");
        }
    }

The problem with CodeB is, that we can not find a member:

var curMember =
                    resolvedDef.Members.FirstOrDefault(m => m.Region.Begin <= cursorLocation && cursorLocation < m.BodyRegion.End);

Is this actually a bug or are we doing something wrong?

Thanks for your help!