NRefactory
NRefactory copied to clipboard
Resolving members with CSHarpTypeResolveContext
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!