Skip to content

Commit 999ce12

Browse files
authored
Merge pull request #35685 from sharwell/refactorings
Implement support for CodeRefactoringProvider in NuGet packages
2 parents d35a8bd + 996119f commit 999ce12

File tree

7 files changed

+306
-23
lines changed

7 files changed

+306
-23
lines changed
Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
22

3-
using System;
4-
using System.Collections.Generic;
3+
using System.Collections.Immutable;
54
using System.Linq;
65
using System.Threading;
76
using System.Threading.Tasks;
7+
using Microsoft.CodeAnalysis.CodeActions;
88
using Microsoft.CodeAnalysis.CodeRefactorings;
9+
using Microsoft.CodeAnalysis.Diagnostics;
910
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
1011
using Microsoft.CodeAnalysis.Extensions;
11-
using Microsoft.CodeAnalysis.Host.Mef;
1212
using Microsoft.CodeAnalysis.Test.Utilities;
1313
using Microsoft.CodeAnalysis.Text;
1414
using Xunit;
@@ -21,19 +21,42 @@ public class CodeRefactoringServiceTest
2121
[Fact]
2222
public async Task TestExceptionInComputeRefactorings()
2323
{
24-
await VerifyRefactoringDisabledAsync(new ErrorCases.ExceptionInCodeActions());
24+
await VerifyRefactoringDisabledAsync<ErrorCases.ExceptionInCodeActions>();
2525
}
2626

2727
[Fact]
2828
public async Task TestExceptionInComputeRefactoringsAsync()
2929
{
30-
await VerifyRefactoringDisabledAsync(new ErrorCases.ExceptionInComputeRefactoringsAsync());
30+
await VerifyRefactoringDisabledAsync<ErrorCases.ExceptionInComputeRefactoringsAsync>();
3131
}
3232

33-
private async Task VerifyRefactoringDisabledAsync(CodeRefactoringProvider codeRefactoring)
33+
[Fact]
34+
public async Task TestProjectRefactoringAsync()
3435
{
35-
var refactoringService = new CodeRefactorings.CodeRefactoringService(GetMetadata(codeRefactoring));
36-
using var workspace = TestWorkspace.CreateCSharp(@"class Program {}");
36+
var code = @"
37+
a
38+
";
39+
40+
using var workspace = TestWorkspace.CreateCSharp(code);
41+
var refactoringService = workspace.GetService<ICodeRefactoringService>();
42+
43+
var reference = new StubAnalyzerReference();
44+
var project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(reference);
45+
var document = project.Documents.Single();
46+
var refactorings = await refactoringService.GetRefactoringsAsync(document, TextSpan.FromBounds(0, 0), CancellationToken.None);
47+
48+
var stubRefactoringAction = refactorings.Single(refactoring => refactoring.CodeActions.FirstOrDefault().action?.Title == nameof(StubRefactoring));
49+
Assert.True(stubRefactoringAction is object);
50+
}
51+
52+
private async Task VerifyRefactoringDisabledAsync<T>()
53+
where T : CodeRefactoringProvider
54+
{
55+
var exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic.WithPart(typeof(T))).CreateExportProvider();
56+
using var workspace = TestWorkspace.CreateCSharp(@"class Program {}", exportProvider: exportProvider);
57+
var refactoringService = workspace.GetService<ICodeRefactoringService>();
58+
var codeRefactoring = exportProvider.GetExportedValues<CodeRefactoringProvider>().OfType<T>().Single();
59+
3760
var project = workspace.CurrentSolution.Projects.Single();
3861
var document = project.Documents.Single();
3962
var extensionManager = document.Project.Solution.Workspace.Services.GetService<IExtensionManager>() as EditorLayerExtensionManager.ExtensionManager;
@@ -42,13 +65,47 @@ private async Task VerifyRefactoringDisabledAsync(CodeRefactoringProvider codeRe
4265
Assert.False(extensionManager.IsIgnored(codeRefactoring));
4366
}
4467

45-
private static IEnumerable<Lazy<CodeRefactoringProvider, CodeChangeProviderMetadata>> GetMetadata(params CodeRefactoringProvider[] providers)
68+
internal class StubRefactoring : CodeRefactoringProvider
4669
{
47-
foreach (var provider in providers)
70+
public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
4871
{
49-
var providerCopy = provider;
50-
yield return new Lazy<CodeRefactoringProvider, CodeChangeProviderMetadata>(() => providerCopy, new CodeChangeProviderMetadata("Test", languages: LanguageNames.CSharp));
72+
context.RegisterRefactoring(CodeAction.Create(
73+
nameof(StubRefactoring),
74+
cancellationToken => Task.FromResult(context.Document),
75+
equivalenceKey: nameof(StubRefactoring)));
76+
77+
return Task.CompletedTask;
5178
}
5279
}
80+
81+
private class StubAnalyzerReference : AnalyzerReference, ICodeRefactoringProviderFactory
82+
{
83+
public readonly CodeRefactoringProvider Refactoring;
84+
85+
public StubAnalyzerReference()
86+
{
87+
Refactoring = new StubRefactoring();
88+
}
89+
90+
public StubAnalyzerReference(CodeRefactoringProvider codeRefactoring)
91+
{
92+
Refactoring = codeRefactoring;
93+
}
94+
95+
public override string Display => nameof(StubAnalyzerReference);
96+
97+
public override string FullPath => string.Empty;
98+
99+
public override object Id => nameof(StubAnalyzerReference);
100+
101+
public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string language)
102+
=> ImmutableArray<DiagnosticAnalyzer>.Empty;
103+
104+
public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
105+
=> ImmutableArray<DiagnosticAnalyzer>.Empty;
106+
107+
public ImmutableArray<CodeRefactoringProvider> GetRefactorings()
108+
=> ImmutableArray.Create(Refactoring);
109+
}
53110
}
54111
}

src/EditorFeatures/Test/CodeRefactorings/ErrorCases/CodeRefactoringExceptionInComputeRefactorings.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
22

33
using System;
4+
using System.Composition;
45
using System.Threading.Tasks;
56
using Microsoft.CodeAnalysis.CodeRefactorings;
7+
using Microsoft.CodeAnalysis.Host.Mef;
68

79
namespace Microsoft.CodeAnalysis.Editor.UnitTests.CodeRefactoringService.ErrorCases
810
{
11+
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = "Test")]
12+
[Shared]
13+
[PartNotDiscoverable]
914
internal class ExceptionInCodeActions : CodeRefactoringProvider
1015
{
16+
[ImportingConstructor]
17+
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
18+
public ExceptionInCodeActions()
19+
{
20+
}
21+
1122
public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
1223
{
1324
throw new Exception($"Exception thrown from ComputeRefactoringsAsync in {nameof(ExceptionInCodeActions)}");

src/EditorFeatures/Test/CodeRefactorings/ErrorCases/CodeRefactoringExceptionInComputeRefactoringsAsync.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
22

33
using System;
4+
using System.Composition;
45
using System.Threading.Tasks;
56
using Microsoft.CodeAnalysis.CodeRefactorings;
7+
using Microsoft.CodeAnalysis.Host.Mef;
68

79
namespace Microsoft.CodeAnalysis.Editor.UnitTests.CodeRefactoringService.ErrorCases
810
{
11+
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = "Test")]
12+
[Shared]
13+
[PartNotDiscoverable]
914
internal class ExceptionInComputeRefactoringsAsync : CodeRefactoringProvider
1015
{
16+
[ImportingConstructor]
17+
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
18+
public ExceptionInComputeRefactoringsAsync()
19+
{
20+
}
21+
1122
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
1223
{
1324
await Task.Yield();

src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs

Lines changed: 127 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
using System.Collections.Immutable;
88
using System.Composition;
99
using System.Linq;
10+
using System.Reflection;
1011
using System.Runtime.CompilerServices;
1112
using System.Threading;
1213
using System.Threading.Tasks;
1314
using Microsoft.CodeAnalysis.CodeActions;
15+
using Microsoft.CodeAnalysis.Diagnostics;
1416
using Microsoft.CodeAnalysis.Extensions;
1517
using Microsoft.CodeAnalysis.Host.Mef;
1618
using Microsoft.CodeAnalysis.Internal.Log;
@@ -24,21 +26,29 @@ namespace Microsoft.CodeAnalysis.CodeRefactorings
2426
[Export(typeof(ICodeRefactoringService)), Shared]
2527
internal class CodeRefactoringService : ICodeRefactoringService
2628
{
27-
private readonly Lazy<ImmutableDictionary<string, Lazy<IEnumerable<CodeRefactoringProvider>>>> _lazyLanguageToProvidersMap;
29+
private readonly Lazy<ImmutableDictionary<string, Lazy<ImmutableArray<CodeRefactoringProvider>>>> _lazyLanguageToProvidersMap;
30+
private readonly ConditionalWeakTable<IReadOnlyList<AnalyzerReference>, StrongBox<ImmutableArray<CodeRefactoringProvider>>> _projectRefactoringsMap
31+
= new ConditionalWeakTable<IReadOnlyList<AnalyzerReference>, StrongBox<ImmutableArray<CodeRefactoringProvider>>>();
32+
33+
private readonly ConditionalWeakTable<AnalyzerReference, ProjectCodeRefactoringProvider> _analyzerReferenceToRefactoringsMap
34+
= new ConditionalWeakTable<AnalyzerReference, ProjectCodeRefactoringProvider>();
35+
private readonly ConditionalWeakTable<AnalyzerReference, ProjectCodeRefactoringProvider>.CreateValueCallback _createProjectCodeRefactoringsProvider
36+
= new ConditionalWeakTable<AnalyzerReference, ProjectCodeRefactoringProvider>.CreateValueCallback(r => new ProjectCodeRefactoringProvider(r));
2837

2938
[ImportingConstructor]
39+
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
3040
public CodeRefactoringService(
3141
[ImportMany] IEnumerable<Lazy<CodeRefactoringProvider, CodeChangeProviderMetadata>> providers)
3242
{
3343
// convert set of all code refactoring providers into a map from language to a lazy initialized list of ordered providers.
34-
_lazyLanguageToProvidersMap = new Lazy<ImmutableDictionary<string, Lazy<IEnumerable<CodeRefactoringProvider>>>>(
44+
_lazyLanguageToProvidersMap = new Lazy<ImmutableDictionary<string, Lazy<ImmutableArray<CodeRefactoringProvider>>>>(
3545
() =>
3646
ImmutableDictionary.CreateRange(
3747
DistributeLanguages(providers)
3848
.GroupBy(lz => lz.Metadata.Language)
39-
.Select(grp => new KeyValuePair<string, Lazy<IEnumerable<CodeRefactoringProvider>>>(
49+
.Select(grp => new KeyValuePair<string, Lazy<ImmutableArray<CodeRefactoringProvider>>>(
4050
grp.Key,
41-
new Lazy<IEnumerable<CodeRefactoringProvider>>(() => ExtensionOrderer.Order(grp).Select(lz => lz.Value))))));
51+
new Lazy<ImmutableArray<CodeRefactoringProvider>>(() => ExtensionOrderer.Order(grp).Select(lz => lz.Value).ToImmutableArray())))));
4252
}
4353

4454
private IEnumerable<Lazy<CodeRefactoringProvider, OrderableLanguageMetadata>> DistributeLanguages(IEnumerable<Lazy<CodeRefactoringProvider, CodeChangeProviderMetadata>> providers)
@@ -54,19 +64,18 @@ private IEnumerable<Lazy<CodeRefactoringProvider, OrderableLanguageMetadata>> Di
5464
}
5565
}
5666

57-
private ImmutableDictionary<string, Lazy<IEnumerable<CodeRefactoringProvider>>> LanguageToProvidersMap
67+
private ImmutableDictionary<string, Lazy<ImmutableArray<CodeRefactoringProvider>>> LanguageToProvidersMap
5868
=> _lazyLanguageToProvidersMap.Value;
5969

60-
private IEnumerable<CodeRefactoringProvider> GetProviders(Document document)
70+
private ConcatImmutableArray<CodeRefactoringProvider> GetProviders(Document document)
6171
{
72+
var allRefactorings = ImmutableArray<CodeRefactoringProvider>.Empty;
6273
if (LanguageToProvidersMap.TryGetValue(document.Project.Language, out var lazyProviders))
6374
{
64-
return lazyProviders.Value;
65-
}
66-
else
67-
{
68-
return SpecializedCollections.EmptyEnumerable<CodeRefactoringProvider>();
75+
allRefactorings = lazyProviders.Value;
6976
}
77+
78+
return allRefactorings.ConcatFast(GetProjectRefactorings(document.Project));
7079
}
7180

7281
public async Task<bool> HasRefactoringsAsync(
@@ -192,5 +201,112 @@ public async Task<ImmutableArray<CodeRefactoring>> GetRefactoringsAsync(
192201

193202
return null;
194203
}
204+
205+
private ImmutableArray<CodeRefactoringProvider> GetProjectRefactorings(Project project)
206+
{
207+
// TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict refactorings in Interactive
208+
if (project.Solution.Workspace.Kind == WorkspaceKind.Interactive)
209+
{
210+
return ImmutableArray<CodeRefactoringProvider>.Empty;
211+
}
212+
213+
if (_projectRefactoringsMap.TryGetValue(project.AnalyzerReferences, out var refactorings))
214+
{
215+
return refactorings.Value;
216+
}
217+
218+
return GetProjectRefactoringsSlow(project);
219+
220+
// Local functions
221+
ImmutableArray<CodeRefactoringProvider> GetProjectRefactoringsSlow(Project project)
222+
{
223+
return _projectRefactoringsMap.GetValue(project.AnalyzerReferences, pId => new StrongBox<ImmutableArray<CodeRefactoringProvider>>(ComputeProjectRefactorings(project))).Value;
224+
}
225+
226+
ImmutableArray<CodeRefactoringProvider> ComputeProjectRefactorings(Project project)
227+
{
228+
var builder = ArrayBuilder<CodeRefactoringProvider>.GetInstance();
229+
foreach (var reference in project.AnalyzerReferences)
230+
{
231+
var projectCodeRefactoringProvider = _analyzerReferenceToRefactoringsMap.GetValue(reference, _createProjectCodeRefactoringsProvider);
232+
foreach (var refactoring in projectCodeRefactoringProvider.GetRefactorings(project.Language))
233+
{
234+
builder.Add(refactoring);
235+
}
236+
}
237+
238+
return builder.ToImmutableAndFree();
239+
}
240+
}
241+
242+
private class ProjectCodeRefactoringProvider
243+
{
244+
private readonly AnalyzerReference _reference;
245+
private ImmutableDictionary<string, ImmutableArray<CodeRefactoringProvider>> _refactoringsPerLanguage;
246+
247+
public ProjectCodeRefactoringProvider(AnalyzerReference reference)
248+
{
249+
_reference = reference;
250+
_refactoringsPerLanguage = ImmutableDictionary<string, ImmutableArray<CodeRefactoringProvider>>.Empty;
251+
}
252+
253+
public ImmutableArray<CodeRefactoringProvider> GetRefactorings(string language)
254+
{
255+
return ImmutableInterlocked.GetOrAdd(ref _refactoringsPerLanguage, language, (language, provider) => provider.CreateRefactorings(language), this);
256+
}
257+
258+
private ImmutableArray<CodeRefactoringProvider> CreateRefactorings(string language)
259+
{
260+
// check whether the analyzer reference knows how to return fixers directly.
261+
if (_reference is ICodeRefactoringProviderFactory codeRefactoringProviderFactory)
262+
{
263+
return codeRefactoringProviderFactory.GetRefactorings();
264+
}
265+
266+
// otherwise, see whether we can pick it up from reference itself
267+
if (!(_reference is AnalyzerFileReference analyzerFileReference))
268+
{
269+
return ImmutableArray<CodeRefactoringProvider>.Empty;
270+
}
271+
272+
var builder = ArrayBuilder<CodeRefactoringProvider>.GetInstance();
273+
274+
try
275+
{
276+
var analyzerAssembly = analyzerFileReference.GetAssembly();
277+
var typeInfos = analyzerAssembly.DefinedTypes;
278+
279+
foreach (var typeInfo in typeInfos)
280+
{
281+
if (typeInfo.IsSubclassOf(typeof(CodeRefactoringProvider)))
282+
{
283+
try
284+
{
285+
var attribute = typeInfo.GetCustomAttribute<ExportCodeRefactoringProviderAttribute>();
286+
if (attribute != null)
287+
{
288+
if (attribute.Languages == null ||
289+
attribute.Languages.Length == 0 ||
290+
attribute.Languages.Contains(language))
291+
{
292+
builder.Add((CodeRefactoringProvider)Activator.CreateInstance(typeInfo.AsType()));
293+
}
294+
}
295+
}
296+
catch
297+
{
298+
}
299+
}
300+
}
301+
}
302+
catch
303+
{
304+
// REVIEW: is the below message right?
305+
// NOTE: We could report "unable to load analyzer" exception here but it should have been already reported by DiagnosticService.
306+
}
307+
308+
return builder.ToImmutableAndFree();
309+
}
310+
}
195311
}
196312
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
using System.Collections.Immutable;
4+
5+
namespace Microsoft.CodeAnalysis.CodeRefactorings
6+
{
7+
internal interface ICodeRefactoringProviderFactory
8+
{
9+
ImmutableArray<CodeRefactoringProvider> GetRefactorings();
10+
}
11+
}

0 commit comments

Comments
 (0)