Repository metrics
- Stars
- (20,414 stars)
- PR merge metrics
- (平均マージ 6d 17h) (30d で 256 merged PRs)
説明
Currently if namespace does not reflect directory structure there is a Light Bulb option to change the namespace element to match the structure. You can also choose to do it for all occurrences in project or solution. Unfortunately, there is no way to do it only in current directory. It would be extremely helpful while refactoring.
I have implemented this locally by allowing the FixAllProvider to return FixAllScope values from GetSupportedFixAllScopes that are outside the FillAllScope enumeration range. By also implementing the interface below the link text for each scope can be controlled and also the dialog text.
internal interface ICustomScopeFixAllProvider { string GetTitle(FixAllScope scope); string GetProgressTitle(FixAllScope fixAllScope, Document triggerDocument, Project triggerProject); }
I could have just added one or more enumeration values to FixAllScope but I was exploring allowing developers to add their own links for the fix all flavors. As you can see from the images above I have 3 new scopes - if they became new enumeration values then the FixAllContext would have helper methods for getting the diagnostics. Given that the "Sync Namespaces" context menu feature also has requests, #74777, #61479 and https://github.com/dotnet/roslyn/issues/57847#issuecomment-1057529705, for more granular selection perhaps it is not necessary to have more than just one ? ( I have implemented this locally and will create a separate request for that )
If you are interested....
Changes made to facilitate custom scopes.
AbstractFixAllCodeAction
public override string Title
=> this.FixAllState.Scope switch
{
FixAllScope.Document => FeaturesResources.Document,
FixAllScope.Project => FeaturesResources.Project,
FixAllScope.Solution => FeaturesResources.Solution,
FixAllScope.ContainingMember => FeaturesResources.Containing_Member,
FixAllScope.ContainingType => FeaturesResources.Containing_Type,
_ => GetCustomScopeTitle(this.FixAllState.Scope, this.FixAllState.FixAllProvider),
};
private static string GetCustomScopeTitle(FixAllScope scope, IFixAllProvider fixAllProvider)
{
if (fixAllProvider is ICustomScopeFixAllProvider customScopeFixAllProvider)
{
return customScopeFixAllProvider.GetTitle(scope);
}
throw ExceptionUtilities.UnexpectedValue(scope);
}
// invoked in ComputeOperationsAsync and GetChangedSolutionAsync
private static string GetProgressTitle(IFixAllContext fixAllContext)
{
if (fixAllContext.Scope.IsCustomScope() && fixAllContext.FixAllProvider is ICustomScopeFixAllProvider customScopeFixAllProvider)
{
return customScopeFixAllProvider.GetProgressTitle(fixAllContext.Scope, fixAllContext.Document!, fixAllContext.Project);
}
return fixAllContext.GetDefaultFixAllTitle();
}
I created an abstract implementation of the ICustomScopeFixAllProvider that deals with registrations of ICustomFixAllScope and mapping between FixAllScope values outside of the range and the ICustomFixAllScope. The ICustomFixAllScope is used for the link titles etc.
internal interface ICustomFixAllScope
{
string Title { get; }
string FixTitle { get; }
Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(FixAllContext fixAllContext);
}
internal abstract class CustomScopeFixAllProvider : FixAllProvider, ICustomScopeFixAllProvider
{
public class CustomFixAllScopes
{
private const int Start = 100;
private readonly List<ICustomFixAllScope> _customFixAllScopes = [];
private class CustomFixAllScope(string title, string fixTitle, Func<FixAllContext, Task<ImmutableArray<Diagnostic>>> diagnosticsProvider) : ICustomFixAllScope
{
public string Title { get; } = title;
public string FixTitle { get; } = fixTitle;
public Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(FixAllContext fixAllContext)
{
return diagnosticsProvider(fixAllContext);
}
}
public IEnumerable<FixAllScope> GetScopes()
{
return Enumerable.Range(Start, _customFixAllScopes.Count).Select(i => (FixAllScope)i);
}
public void Add(ICustomFixAllScope customFillAllScope)
{
_customFixAllScopes.Add(customFillAllScope);
}
public void Add(string title, string fixTitle, Func<FixAllContext, Task<ImmutableArray<Diagnostic>>> diagnosticsProvider)
{
Add(new CustomFixAllScope(title, fixTitle, diagnosticsProvider));
}
public string GetTitle(FixAllScope fixAllScope)
{
return GetCustomScope(fixAllScope).Title;
}
public ICustomFixAllScope GetCustomScope(FixAllScope fixAllScope)
{
var customScope = (int)fixAllScope;
return _customFixAllScopes[customScope - Start];
}
}
private CustomFixAllScopes? _customFixAllScopes;
public override IEnumerable<FixAllScope> GetSupportedFixAllScopes()
{
_customFixAllScopes = new CustomFixAllScopes();
AddCustomScopes(_customFixAllScopes);
return GetNonCustomFixAllScopes().Concat(_customFixAllScopes.GetScopes());
}
protected ICustomFixAllScope GetCustomFixAllScope(FixAllScope fixAllScope)
{
if (_customFixAllScopes == null)
{
throw new InvalidOperationException("CustomFixAllScopes not initialized.");
}
return _customFixAllScopes.GetCustomScope(fixAllScope);
}
protected virtual IEnumerable<FixAllScope> GetNonCustomFixAllScopes() => base.GetSupportedFixAllScopes();
protected abstract void AddCustomScopes(CustomFixAllScopes customFixAllScopes);
public string GetTitle(FixAllScope scope) => _customFixAllScopes!.GetTitle(scope);
protected virtual async Task<ImmutableArray<Diagnostic>> GetBuiltInScopeDiagnosticsAsync(FixAllContext fixAllContext)
{
var diagnostics = fixAllContext.Scope switch
{
FixAllScope.Document when fixAllContext.Document is not null => await fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Document).ConfigureAwait(false),
FixAllScope.Project => await fixAllContext.GetAllDiagnosticsAsync(fixAllContext.Project).ConfigureAwait(false),
FixAllScope.Solution => await GetSolutionDiagnosticsAsync(fixAllContext).ConfigureAwait(false),
_ => throw new NotSupportedException($"Unsupported FixAllScope: {fixAllContext.Scope}")
};
return diagnostics;
static async Task<ImmutableArray<Diagnostic>> GetSolutionDiagnosticsAsync(FixAllContext fixAllContext)
{
var diagnostics = ImmutableArray.CreateBuilder<Diagnostic>();
foreach (var project in fixAllContext.Solution.Projects)
{
var projectDiagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
diagnostics.AddRange(projectDiagnostics);
}
return diagnostics.ToImmutableAndClear();
}
}
private async Task<(string Title, ImmutableArray<Diagnostic> Diagnostics)> GetFixTitleAndDiagnosticsAsync(FixAllContext fixAllContext)
{
if (fixAllContext.Scope.IsCustomScope())
{
var customFixAllScope = _customFixAllScopes!.GetCustomScope(fixAllContext.Scope);
return (customFixAllScope.FixTitle, await customFixAllScope.GetDiagnosticsAsync(fixAllContext).ConfigureAwait(false));
}
var diagnostics = await GetBuiltInScopeDiagnosticsAsync(fixAllContext).ConfigureAwait(false);
return (fixAllContext.GetDefaultFixAllTitle(), diagnostics);
}
public override async Task<CodeAction?> GetFixAsync(FixAllContext fixAllContext)
{
var titleDiagnostics = await GetFixTitleAndDiagnosticsAsync(fixAllContext).ConfigureAwait(false);
if (titleDiagnostics.Diagnostics.IsDefaultOrEmpty)
return null;
return CodeAction.Create(
titleDiagnostics.Title,
cancellationToken => FixAsync(
fixAllContext.Project.Solution,
titleDiagnostics.Diagnostics,
fixAllContext.Progress,
cancellationToken),
titleDiagnostics.Title);
}
public virtual string GetProgressTitle(FixAllScope fixAllScope,
Document triggerDocument,
Project triggerProject)
{
var customScope = GetCustomFixAllScope(fixAllScope);
return customScope.FixTitle;
}
protected abstract Task<Solution> FixAsync(Solution solution,
ImmutableArray<Diagnostic> diagnostics,
IProgress<CodeAnalysisProgress> progressTracker,
CancellationToken cancellationToken);
}
internal static class FixAllCustomScopeExtensions
{
private static readonly int s_maxValue = Enum.GetValues(typeof(FixAllScope)).Cast<int>().Max();
public static bool IsCustomScope(this FixAllScope scope)
{
return (int)scope > s_maxValue;
}
}
For issue #57847
internal abstract partial class AbstractChangeNamespaceToMatchFolderCodeFixProvider
{
private sealed class CustomFixAllProvider : CustomScopeFixAllProvider
{
public static readonly CustomFixAllProvider Instance = new();
protected override void AddCustomScopes(CustomFixAllScopes customFixAllScopes)
{
customFixAllScopes.Add("Directory", "Change namespace to match folder structure - all files in directory", async fixAllContext =>
{
return await GetDirectoryDiagnosticsAsync(fixAllContext, d => d.Folders.SequenceEqual(fixAllContext.Document!.Folders)).ConfigureAwait(false);
});
customFixAllScopes.Add("Directory + Descendants", "Change namespace to match folder structure - all files in directory and below", async fixAllContext =>
{
var documentDirectory = PathUtilities.GetDirectoryName(fixAllContext.Document!.FilePath!);
return await GetDirectoryDiagnosticsAsync(fixAllContext, d =>
{
var directoryName = PathUtilities.GetDirectoryName(d.FilePath!);
return PathUtilities.IsSameDirectoryOrChildOf(directoryName, documentDirectory);
}).ConfigureAwait(false);
});
customFixAllScopes.Add("Directory + Ascendants", "Change namespace to match folder structure - all files in directory and above", async fixAllContext =>
{
var documentDirectory = PathUtilities.GetDirectoryName(fixAllContext.Document!.FilePath!);
return await GetDirectoryDiagnosticsAsync(fixAllContext, d =>
{
var directoryName = PathUtilities.GetDirectoryName(d.FilePath!);
return PathUtilities.IsSameDirectoryOrChildOf(documentDirectory, directoryName);
}).ConfigureAwait(false);
});
}
private static async Task<ImmutableArray<Diagnostic>> GetDirectoryDiagnosticsAsync(FixAllContext fixAllContext, Func<Document, bool> predicate)
{
var document = fixAllContext.Document;
if (document != null && document.FilePath != null)
{
var documentsInDirectory = document.Project.Documents.Where(d => d.FilePath != null).Where(predicate);
var directoryDiagnosticTasks = documentsInDirectory.Select(ad => fixAllContext.GetDocumentDiagnosticsAsync(ad));
var directoryDiagnostics = await Task.WhenAll(directoryDiagnosticTasks).ConfigureAwait(false);
var builder = ImmutableArray.CreateBuilder<Diagnostic>();
foreach (var result in directoryDiagnostics)
{
builder.AddRange(result);
}
return builder.ToImmutable();
}
return [];
}
protected override Task<Solution> FixAsync(Solution solution, ImmutableArray<Diagnostic> diagnostics, IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
{
// existing code
return FixAllByDocumentAsync(
solution,
diagnostics,
progressTracker,
cancellationToken);
}