Skip to content

Commit

Permalink
Merge pull request #1969 from filipw/feature/extract-class
Browse files Browse the repository at this point in the history
Added support for 'extract base class'
  • Loading branch information
filipw authored Oct 6, 2020
2 parents 2383c7c + e79d1e4 commit d51bdea
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 2 deletions.
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
// 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

0 comments on commit d51bdea

Please sign in to comment.