Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for 'extract base class' #1969

Merged
merged 5 commits into from
Oct 6, 2020
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Composition;
using System.Reflection;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;

namespace OmniSharp
{
[Shared]
[ExportWorkspaceServiceFactoryWithAssemblyQualifiedName("Microsoft.CodeAnalysis.Features", "Microsoft.CodeAnalysis.ExtractClass.IExtractClassOptionsService")]
public class ExtractClassOptionsServiceWorkspaceServiceFactory : IWorkspaceServiceFactory
{
public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
{
// Generates proxy class to get around issue that IExtractClassOptionsService is internal at this point.
var internalType = Assembly.Load("Microsoft.CodeAnalysis.Features").GetType("Microsoft.CodeAnalysis.ExtractClass.IExtractClassOptionsService");
return (IWorkspaceService)typeof(DispatchProxy).GetMethod(nameof(DispatchProxy.Create)).MakeGenericMethod(internalType, typeof(ExtractClassWorkspaceService)).Invoke(null, null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;

namespace OmniSharp
{
public class ExtractClassWorkspaceService : DispatchProxy
{
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
// the args correspond to the following interface method on IExtractClassOptionsService
// http://sourceroslyn.io/#Microsoft.CodeAnalysis.Features/ExtractClass/IExtractClassOptionsService.cs,30b2d7f792fbcc68
// internal interface IExtractClassOptionsService : IWorkspaceService
// {
// Task<ExtractClassOptions?> GetExtractClassOptionsAsync(Document document, INamedTypeSymbol originalType, ISymbol? selectedMember);
// }
// if it changes, this implementation must be changed accordingly

var featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features");
var extractClassOptionsType = featuresAssembly.GetType("Microsoft.CodeAnalysis.ExtractClass.ExtractClassOptions");
var extractClassMemberAnalysisResultType = featuresAssembly.GetType("Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult");

// we need to create Enumerable.Cast<ExtractClassMemberAnalysisResult>() where ExtractClassMemberAnalysisResult is not accessible publicly
var genericCast = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(extractClassMemberAnalysisResultType);

var originalType = (INamedTypeSymbol)args[1];
var selectedSymbol = (ISymbol)args[2];

var symbolsToUse = selectedSymbol == null ? originalType.GetMembers().Where(member => member switch
{
IMethodSymbol methodSymbol => methodSymbol.MethodKind == MethodKind.Ordinary,
IFieldSymbol fieldSymbol => !fieldSymbol.IsImplicitlyDeclared,
_ => member.Kind == SymbolKind.Property || member.Kind == SymbolKind.Event
}).ToArray() : new ISymbol[1] { selectedSymbol };

// we need to create ImmutableArray.CreateRange<ExtractClassMemberAnalysisResult>() where ExtractClassMemberAnalysisResult is not accessible publicly
var extractClassMemberAnalysisResultImmutableArray = typeof(ImmutableArray).GetMethods()
.Where(x => x.Name == "CreateRange")
.Select(method => new
{
method,
parameters = method.GetParameters(),
genericArguments = method.GetGenericArguments()
})
.Where(method => method.genericArguments.Length == 1 && method.parameters.Length == 1)
.Select(x => x.method)
.First()
.MakeGenericMethod(extractClassMemberAnalysisResultType).Invoke(null, new object[]
{
// at this point we have IEnumerable<object> and need to cast to IEnumerable<ExtractClassMemberAnalysisResult>
// which we can then pass to ImmutableArray.CreateRange<ExtractClassMemberAnalysisResult>()
genericCast.Invoke(null, new object[]
{
// this constructor corresponds to
// http://sourceroslyn.io/#Microsoft.CodeAnalysis.Features/ExtractClass/ExtractClassOptions.cs,ced9042e0a010e24
// public ExtractClassMemberAnalysisResult(ISymbol member,bool makeAbstract)
// if it changes, this implementation must be changed accordingly
symbolsToUse.Select(symbol => Activator.CreateInstance(extractClassMemberAnalysisResultType, new object[]
{
symbol,
false
}))
})
});

const string name = "NewBaseType";

// this constructor corresponds to
// http://sourceroslyn.io/#Microsoft.CodeAnalysis.Features/ExtractClass/ExtractClassOptions.cs,6f65491c71285819,references
// public ExtractClassOptions(string fileName, string typeName, bool sameFile, ImmutableArray<ExtractClassMemberAnalysisResult> memberAnalysisResults)
// if it changes, this implementation must be changed accordingly
var resultObject = Activator.CreateInstance(extractClassOptionsType, new object[] {
$"{name}.cs",
name,
true,
extractClassMemberAnalysisResultImmutableArray
});

// the return type is Task<ExtractClassOptions>
return typeof(Task).GetMethod("FromResult").MakeGenericMethod(extractClassOptionsType).Invoke(null, new[] { resultObject });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
return (IWorkspaceService)typeof(DispatchProxy).GetMethod(nameof(DispatchProxy.Create)).MakeGenericMethod(internalType, typeof(ExtractInterfaceWorkspaceService)).Invoke(null, null);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ protected override object Invoke(MethodInfo targetMethod, object[] args)
return Activator.CreateInstance(resultTypeInternal, new object[] { args[1], args[2] });
}
}
}
}
57 changes: 57 additions & 0 deletions tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,63 @@ public CodeActionsWithOptionsFacts(ITestOutputHelper output)
{
}

[Fact]
public async Task Can_extract_base_class()
{
const string code =
@"public class Class1[||]
{
public string Property { get; set; }

public void Method() { }
}";
const string expected =
@"public class NewBaseType
{
public string Property { get; set; }

public void Method() { }
}

public class Class1 : NewBaseType
{
}";

// TODO: this refactoring was renamed to 'Extract base class...' in latest Roslyn
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for documenting this!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the next lucky winner that updates Roslyn 😀

// it needs to be updated accordingly when we pull in new Roslyn build
// https://github.com/dotnet/roslyn/commit/2d98f81de3908f39cd582d3de0c51c738c558700
var response = await RunRefactoringAsync(code, "Pull member(s) up to new base class...");
AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer);
}

[Fact]
public async Task Can_extract_base_class_from_specific_member()
{
const string code =
@"public class Class1
{
public string Property[||] { get; set; }

public void Method() { }
}";
const string expected =
@"public class NewBaseType
{
public string Property { get; set; }
}

public class Class1 : NewBaseType
{
public void Method() { }
}";

// TODO: this refactoring was renamed to 'Extract base class...' in latest Roslyn
// it needs to be updated accordingly when we pull in new Roslyn build
// https://github.com/dotnet/roslyn/commit/2d98f81de3908f39cd582d3de0c51c738c558700
var response = await RunRefactoringAsync(code, "Pull member(s) up to new base class...");
AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer);
}

[Fact]
public async Task Can_generate_constructor_with_default_arguments()
{
Expand Down