|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ChangeNamespace;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace;
internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax>
: CodeRefactoringProvider
where TNamespaceDeclarationSyntax : SyntaxNode
where TCompilationUnitSyntax : SyntaxNode
where TMemberDeclarationSyntax : SyntaxNode
{
internal sealed class State
{
/// <summary>
/// The document in which the refactoring is triggered.
/// </summary>
public Document Document { get; }
/// <summary>
/// The applicable container node based on cursor location,
/// which will be used to change namespace.
/// </summary>
public SyntaxNode Container { get; }
/// <summary>
/// This is the new name we want to change the namespace to.
/// Empty string means global namespace, whereas null means change namespace action is not available.
/// </summary>
public string? TargetNamespace { get; }
/// <summary>
/// This is the part of the declared namespace that is contained in default namespace.
/// We will use this to construct target folder to move the file to.
/// For example, if default namespace is `A` and declared namespace is `A.B.C`,
/// this would be `B.C`.
/// </summary>
public string? RelativeDeclaredNamespace { get; }
private State(
Document document,
SyntaxNode container,
string? targetNamespace,
string? relativeDeclaredNamespace)
{
Document = document;
Container = container;
TargetNamespace = targetNamespace;
RelativeDeclaredNamespace = relativeDeclaredNamespace;
}
public static async Task<State?> CreateAsync(
AbstractSyncNamespaceCodeRefactoringProvider<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax> provider,
Document document,
TextSpan textSpan,
CancellationToken cancellationToken)
{
// User must put cursor on one of the nodes described below to trigger the refactoring.
// For each scenario, all requirements must be met. Some of them are checked by `TryGetApplicableInvocationNodeAsync`,
// rest by `IChangeNamespaceService.CanChangeNamespaceAsync`.
//
// - A namespace declaration node that is the only namespace declaration in the document and all types are declared in it:
// 1. No nested namespace declarations (even it's empty).
// 2. The cursor is on the name of the namespace declaration.
// 3. The name of the namespace is valid (i.e. no errors).
// 4. No partial type declared in the namespace. Otherwise its multiple declaration will
// end up in different namespace.
//
// - A compilation unit node that contains no namespace declaration:
// 1. The cursor is on the name of first declared type.
// 2. No partial type declared in the document. Otherwise its multiple declaration will
// end up in different namespace.
var applicableNode = await provider.TryGetApplicableInvocationNodeAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
if (applicableNode == null)
{
return null;
}
var changeNamespaceService = document.GetRequiredLanguageService<IChangeNamespaceService>();
var canChange = await changeNamespaceService.CanChangeNamespaceAsync(document, applicableNode, cancellationToken).ConfigureAwait(false);
if (!canChange || !IsDocumentPathRootedInProjectFolder(document))
{
return null;
}
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
// We can't determine what the expected namespace would be without knowing the default namespace.
var defaultNamespace = GetDefaultNamespace(document, syntaxFacts);
if (defaultNamespace == null)
{
return null;
}
string declaredNamespace;
if (applicableNode is TCompilationUnitSyntax)
{
declaredNamespace = string.Empty;
}
else if (applicableNode is TNamespaceDeclarationSyntax)
{
var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
declaredNamespace = syntaxGenerator.GetName(applicableNode);
}
else
{
throw ExceptionUtilities.Unreachable();
}
// Namespace can't be changed if we can't construct a valid qualified identifier from folder names.
// In this case, we might still be able to provide refactoring to move file to new location.
var targetNamespace = PathMetadataUtilities.TryBuildNamespaceFromFolders(document.Folders, syntaxFacts, defaultNamespace);
// No action required if namespace already matches folders.
if (syntaxFacts.StringComparer.Equals(targetNamespace, declaredNamespace))
{
return null;
}
// Only provide "move file" action if default namespace contains declared namespace.
// For example, if the default namespace is `Microsoft.CodeAnalysis`, and declared
// namespace is `System.Diagnostics`, it's very likely this document is an outlier
// in the project and user probably has some special rule for it.
var relativeNamespace = GetRelativeNamespace(defaultNamespace, declaredNamespace, syntaxFacts);
return new State(document, applicableNode, targetNamespace, relativeNamespace);
}
/// <summary>
/// Determines if the actual file path matches its logical path in project
/// which is constructed as [project_root_path]\Logical\Folders\. The refactoring
/// is triggered only when the two match. The reason of doing this is we don't really know
/// the user's intention of keeping the file path out-of-sync with its logical path.
/// </summary>
private static bool IsDocumentPathRootedInProjectFolder(Document document)
{
var absoluteDirectoryPath = PathUtilities.GetDirectoryName(document.FilePath);
if (absoluteDirectoryPath is null)
return false;
var projectRoot = PathUtilities.GetDirectoryName(document.Project.FilePath);
if (projectRoot is null)
return false;
var folderPath = Path.Combine([.. document.Folders]);
var logicalDirectoryPath = PathUtilities.CombineAbsoluteAndRelativePaths(projectRoot, folderPath);
if (logicalDirectoryPath is null)
return false;
return PathUtilities.PathsEqual(absoluteDirectoryPath, logicalDirectoryPath);
}
private static string? GetDefaultNamespace(Document document, ISyntaxFactsService syntaxFacts)
{
var solution = document.Project.Solution;
var linkedIds = document.GetLinkedDocumentIds();
var documents = linkedIds.SelectAsArray(solution.GetRequiredDocument).Add(document);
// For all projects containing all the linked documents, bail if
// 1. Any of them doesn't have default namespace, or
// 2. Multiple default namespace are found. (this might be possible by tweaking project file).
// The refactoring depends on a single default namespace to operate.
var defaultNamespaceFromProjects = new HashSet<string?>(
documents.Select(d => d.Project.DefaultNamespace),
syntaxFacts.StringComparer);
if (defaultNamespaceFromProjects.Count > 1)
return null;
return defaultNamespaceFromProjects.SingleOrDefault();
}
/// <summary>
/// Try get the relative namespace for <paramref name="namespace"/> based on <paramref name="relativeTo"/>,
/// if <paramref name="relativeTo"/> is the containing namespace of <paramref name="namespace"/>. Otherwise,
/// Returns null.
/// For example:
/// - If <paramref name="relativeTo"/> is "A.B" and <paramref name="namespace"/> is "A.B.C.D", then
/// the relative namespace is "C.D".
/// - If <paramref name="relativeTo"/> is "A.B" and <paramref name="namespace"/> is also "A.B", then
/// the relative namespace is "".
/// - If <paramref name="relativeTo"/> is "" then the relative namespace us <paramref name="namespace"/>.
/// </summary>
private static string? GetRelativeNamespace(string relativeTo, string @namespace, ISyntaxFactsService syntaxFacts)
{
Debug.Assert(relativeTo != null && @namespace != null);
if (syntaxFacts.StringComparer.Equals(@namespace, relativeTo))
{
return string.Empty;
}
else if (relativeTo.Length == 0)
{
return @namespace;
}
else if (relativeTo.Length >= @namespace.Length)
{
return null;
}
var containingText = relativeTo + ".";
var namespacePrefix = @namespace[..containingText.Length];
return syntaxFacts.StringComparer.Equals(containingText, namespacePrefix)
? @namespace[(relativeTo.Length + 1)..]
: null;
}
}
}
|