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
54 changes: 54 additions & 0 deletions src/VisualStudio/LanguageService.Tests/ForeignProjectsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using XSharp.LanguageService.ForeignSource;
using Xunit;

namespace XSharp.LanguageService.Tests
{
public class ForeignProjectsTests
{
private static KeyValuePair<string, string>[] Outputs(params (string project, string dll)[] pairs)
{
var result = new KeyValuePair<string, string>[pairs.Length];
for (int i = 0; i < pairs.Length; i++)
result[i] = new KeyValuePair<string, string>(pairs[i].project, pairs[i].dll);
return result;
}

[Fact]
public void FindsTheProjectForItsOutput()
{
var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll"));
Assert.Equal(@"C:\s\Lib\Lib.csproj", ForeignProjects.FindProjectFile(outputs, @"C:\s\Lib\bin\Debug\Lib.dll"));
}

[Fact]
public void MatchesDespiteCaseAndRelativeSegments()
{
var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll"));
Assert.Equal(@"C:\s\Lib\Lib.csproj", ForeignProjects.FindProjectFile(outputs, @"c:\S\lib\bin\Release\..\Debug\LIB.dll"));
}

[Fact]
public void XSharpProjectsAreIgnored()
{
var outputs = Outputs((@"C:\s\App\App.xsproj", @"C:\s\App\bin\App.dll"), (@"C:\s\Old\Old.xsprj", @"C:\s\Old\bin\Old.dll"));
Assert.Null(ForeignProjects.FindProjectFile(outputs, @"C:\s\App\bin\App.dll"));
Assert.Null(ForeignProjects.FindProjectFile(outputs, @"C:\s\Old\bin\Old.dll"));
}

[Fact]
public void UnknownDllGivesNull()
{
var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll"));
Assert.Null(ForeignProjects.FindProjectFile(outputs, @"C:\s\Other\bin\Debug\Other.dll"));
}

[Fact]
public void NullOrEmptyAssemblyGivesNull()
{
var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll"));
Assert.Null(ForeignProjects.FindProjectFile(outputs, null));
Assert.Null(ForeignProjects.FindProjectFile(outputs, ""));
}
}
}
181 changes: 181 additions & 0 deletions src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using XSharp.LanguageService.ForeignSource;
using Xunit;

namespace XSharp.LanguageService.Tests
{
public class ForeignSymbolResolverTests
{
private const string CratePath = @"C:\lib\Crate.cs";

// one declaration per line, so that the expected (0-based) line numbers can be read off
private static readonly string CrateSource = string.Join("\n",
"namespace Lib {", // 0
" public class Crate {", // 1
" public Crate(int size) { }", // 2
" public void Open() { }", // 3
" public void Open(string key) { }", // 4
" public int Size { get; set; }", // 5
" public T Get<T>(int index) { return default(T); }",// 6
" public class Lid { }", // 7
" }", // 8
" public interface IBox { void Close(); }", // 9
" public enum Color { Red, Green }", // 10
" public class Plain { }", // 11
"}");

private static Compilation Compile(params (string path, string source)[] files)
{
var trees = files.Select(f => CSharpSyntaxTree.ParseText(f.source, path: f.path));
return CSharpCompilation.Create("Lib", trees,
new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) },
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}

private static IReadOnlyList<ForeignLocation> Resolve(ForeignSymbolKey key)
{
return ForeignSymbolResolver.Resolve(new[] { Compile((CratePath, CrateSource)) }, key);
}

private static ForeignSymbolKey Type(string typeDocId) => new ForeignSymbolKey(typeDocId, null, null, 0);

private static ForeignSymbolKey Member(string typeDocId, string memberDocId, string name, int parameterCount)
=> new ForeignSymbolKey(typeDocId, memberDocId, name, parameterCount);

private static void AssertSingle(IReadOnlyList<ForeignLocation> found, int line)
{
Assert.Single(found);
Assert.Equal(CratePath, found[0].FilePath);
Assert.Equal(line, found[0].StartLine);
}

[Fact]
public void TypeResolvesToItsDeclaration()
{
var found = Resolve(Type("T:Lib.Crate"));
AssertSingle(found, 1);
Assert.Equal("Crate", found[0].Name);
}

[Fact]
public void OverloadPickedByDocId()
{
AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.Open(System.String)", "Open", 1)), 4);
}

[Fact]
public void PropertyResolves()
{
AssertSingle(Resolve(Member("T:Lib.Crate", "P:Lib.Crate.Size", "Size", 0)), 5);
}

[Fact]
public void NestedTypeResolves()
{
AssertSingle(Resolve(Type("T:Lib.Crate.Lid")), 7);
}

[Fact]
public void NestedTypeInMetadataSpellingResolves()
{
// X# takes the type name from Mono.Cecil, which separates nested types with '/' (reflection: '+')
AssertSingle(Resolve(Type("T:Lib.Crate/Lid")), 7);
AssertSingle(Resolve(Type("T:Lib.Crate+Lid")), 7);
}

[Fact]
public void InterfaceAndEnumResolve()
{
AssertSingle(Resolve(Type("T:Lib.IBox")), 9);
AssertSingle(Resolve(Type("T:Lib.Color")), 10);
}

[Fact]
public void ConstructorResolves()
{
AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.#ctor(System.Int32)", ".ctor", 1)), 2);
}

[Fact]
public void UnresolvedDocIdFallsBackToNameAndParameterCount()
{
// the X# doc ID misses the ``1 of the generic method
AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.Get(System.Int32)", "Get", 1)), 6);
}

[Fact]
public void UnknownMemberFallsBackToType()
{
AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.Gone", "Gone", 0)), 1);
}

[Fact]
public void ImplicitConstructorGoesToType()
{
AssertSingle(Resolve(Member("T:Lib.Plain", "M:Lib.Plain.#ctor", ".ctor", 0)), 11);
}

[Fact]
public void PartialClassGivesBothParts()
{
var compilation = Compile(
(@"C:\lib\A.cs", "namespace Lib { public partial class Duo { } }"),
(@"C:\lib\B.cs", "namespace Lib { public partial class Duo { public void M() { } } }"));
var found = ForeignSymbolResolver.Resolve(new[] { compilation }, Type("T:Lib.Duo"));
Assert.Equal(new[] { @"C:\lib\A.cs", @"C:\lib\B.cs" }, found.Select(f => f.FilePath).OrderBy(p => p).ToArray());
}

// a source generator's tree has a virtual path; nothing on disk can be opened there
private const string GeneratedPath = @"Generator\Gen\Duo.g.cs";

private static Compilation CompileWithGeneratedPart()
{
return Compile(
(@"C:\lib\A.cs", "namespace Lib { public partial class Duo { } }"),
(GeneratedPath, "namespace Lib { public partial class Duo { public int Name { get; set; } } }"));
}

private static bool OnDisk(string path) => path != GeneratedPath;

[Fact]
public void GeneratedMemberFallsBackToType()
{
var found = ForeignSymbolResolver.Resolve(new[] { CompileWithGeneratedPart() },
Member("T:Lib.Duo", "P:Lib.Duo.Name", "Name", 0), OnDisk);
Assert.Single(found);
Assert.Equal(@"C:\lib\A.cs", found[0].FilePath);
}

[Fact]
public void PartialTypeDropsGeneratedPart()
{
var found = ForeignSymbolResolver.Resolve(new[] { CompileWithGeneratedPart() }, Type("T:Lib.Duo"), OnDisk);
Assert.Single(found);
Assert.Equal(@"C:\lib\A.cs", found[0].FilePath);
}

[Fact]
public void NothingOnDiskGivesNothing()
{
Assert.Empty(ForeignSymbolResolver.Resolve(new[] { Compile((CratePath, CrateSource)) }, Type("T:Lib.Crate"), p => false));
}

[Fact]
public void UnknownTypeGivesNothing()
{
Assert.Empty(Resolve(Type("T:Lib.Missing")));
}

[Fact]
public void DuplicatesAcrossCompilationsCollapse()
{
// a multi-targeted project: one Roslyn project (compilation) per target framework, same files
var found = ForeignSymbolResolver.Resolve(
new[] { Compile((CratePath, CrateSource)), Compile((CratePath, CrateSource)) }, Type("T:Lib.Crate"));
AssertSingle(found, 1);
}
}
}
55 changes: 55 additions & 0 deletions src/VisualStudio/LanguageService.Tests/GotoDefinitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,60 @@ public void TheTypeStaysNextToAKeyword()
var type = new XSourceTypeSymbol("Crate", Kind.Class, Modifiers.Public, new TextRange(1, 1, 10, 1), new TextInterval(0, 100), null, null);
Assert.Equal(new IXSymbol[] { type }, XSharpGotoDefinition.Targets(new List<IXSymbol> { new XKeywordSymbol("CLASS"), type }));
}

private static XSourceMemberSymbol Method(string name, params (string Name, string Type)[] parameters)
{
var member = new XSourceMemberSymbol(name, Kind.Method, Modifiers.Public, new TextRange(2, 1, 3, 1), new TextInterval(0, 10), "VOID", null);
member.CallingConvention = CallingConvention.None;
foreach (var p in parameters)
{
var par = new XSourceParameterSymbol(member, p.Name, new TextRange(2, 1, 2, 1), new TextInterval(0, 1), p.Type);
par.ParamType = ParamType.As;
member.AddParameter(par);
}
return member;
}

// F12 with the caret in <name> of <text>; identifiers have no known type
private static IList<IXSymbol> PickAt(string text, string name, params IXSymbol[] overloads)
{
var tokens = TestLexer.Lex(text);
return XSharpGotoDefinition.Overloads(overloads, tokens, text.IndexOf(name) + 1, t => null);
}

private static readonly XSourceMemberSymbol OpenNone = Method("Open");
private static readonly XSourceMemberSymbol OpenKey = Method("Open", ("key", "STRING"));
private static readonly XSourceMemberSymbol OpenNumber = Method("Open", ("number", "INT"));

[Fact]
public void TheArgumentCountPicksTheOverload()
{
Assert.Equal(new IXSymbol[] { OpenKey }, PickAt("c:Open(x)\n", "Open", OpenNone, OpenKey));
Assert.Equal(new IXSymbol[] { OpenNone }, PickAt("c:Open()\n", "Open", OpenNone, OpenKey));
}

[Fact]
public void TheArgumentTypePicksTheOverload()
{
Assert.Equal(new IXSymbol[] { OpenKey }, PickAt("c:Open(\"x\")\n", "Open", OpenNone, OpenKey, OpenNumber));
}

[Fact]
public void UnknownArgumentTypesKeepTheCandidates()
{
Assert.Equal(new IXSymbol[] { OpenKey, OpenNumber }, PickAt("c:Open(x)\n", "Open", OpenNone, OpenKey, OpenNumber));
}

[Fact]
public void WithoutACallAllOverloadsStay()
{
Assert.Equal(new IXSymbol[] { OpenNone, OpenKey }, PickAt("x := c:Open\n", "Open", OpenNone, OpenKey));
}

[Fact]
public void ASingleTargetIsKept()
{
Assert.Equal(new IXSymbol[] { OpenKey }, PickAt("c:Open()\n", "Open", OpenKey));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<!-- same version as LanguageService2022, otherwise MSB3277 against the 7.0 that Roslyn 4.8 brings -->
<PackageReference Include="System.Collections.Immutable" Version="10.0.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using XSharp.LanguageService.FindReferences;
using XSharp.LanguageService.ForeignSource;
using XSharp.LanguageService.GoToImplementation;
using XSharp.Settings;
using XSharp.Support;
Expand Down Expand Up @@ -172,6 +173,22 @@ private static void Navigate(object result, SourceHierarchy hierarchy, ITextView
XSharpGotoDefinition.Goto(symbol, textView, state);
}

/// <summary>One row for a DLL symbol from a C# or VB project of the solution: its declaration in that source.</summary>
private static ReferenceHit ForeignHit(IXSymbol symbol)
{
IReadOnlyList<ForeignLocation> found = null;
if (symbol is XPETypeSymbol peType)
found = ForeignNavigator.Find(peType, peType);
else if (symbol is XPEMemberSymbol peMember && peMember.Parent is XPETypeSymbol parent)
found = ForeignNavigator.Find(parent, peMember);
if (found == null || found.Count == 0)
return null;
var first = found[0];
var lineText = System.IO.File.Exists(first.FilePath)
? System.IO.File.ReadLines(first.FilePath).Skip(first.StartLine).FirstOrDefault() : null;
return DeclarationHit.Create(first.FilePath, null, first.StartLine, first.StartColumn, lineText, first.Name, first.ContainerName);
}

/// <summary>One row: the declaration, for a DLL symbol in the generated source F12 opens.</summary>
private static ReferenceHit Hit(object result, SourceHierarchy hierarchy)
{
Expand All @@ -182,6 +199,9 @@ private static ReferenceHit Hit(object result, SourceHierarchy hierarchy)
row.StartLine, row.StartColumn, row.SourceCode, node.Name, node.FullName);
}
var symbol = result as IXSymbol;
var foreign = ForeignHit(symbol);
if (foreign != null)
return foreign;
var entity = symbol as XSourceEntity;
if (entity == null && symbol is XPETypeSymbol peType)
entity = XSharpGotoDefinition.FindSystemElement(peType, peType);
Expand Down
Loading