File: src\Analyzers\Core\Analyzers\MatchFolderAndNamespace\AbstractMatchFolderAndNamespaceDiagnosticAnalyzer.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Analyzers.MatchFolderAndNamespace;
 
internal abstract class AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<TSyntaxKind, TNamespaceSyntax>
    : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    where TSyntaxKind : struct
    where TNamespaceSyntax : SyntaxNode
{
    private static readonly LocalizableResourceString s_localizableTitle = new(
     nameof(AnalyzersResources.Namespace_does_not_match_folder_structure), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
 
    private static readonly LocalizableResourceString s_localizableInsideMessage = new(
        nameof(AnalyzersResources.Namespace_0_does_not_match_folder_structure_expected_1), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
 
    private static readonly SymbolDisplayFormat s_namespaceDisplayFormat = SymbolDisplayFormat
        .FullyQualifiedFormat
        .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted);
 
    protected AbstractMatchFolderAndNamespaceDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.MatchFolderAndNamespaceDiagnosticId,
            EnforceOnBuildValues.MatchFolderAndNamespace,
            CodeStyleOptions2.PreferNamespaceAndFolderMatchStructure,
            s_localizableTitle,
            s_localizableInsideMessage)
    {
    }
 
    protected abstract ISyntaxFacts GetSyntaxFacts();
    protected abstract ImmutableArray<TSyntaxKind> GetSyntaxKindsToAnalyze();
 
    protected sealed override void InitializeWorker(AnalysisContext context)
        => context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, GetSyntaxKindsToAnalyze());
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
    private void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context)
    {
        var option = context.GetAnalyzerOptions().PreferNamespaceAndFolderMatchStructure;
        if (!option.Value || ShouldSkipAnalysis(context, option.Notification))
        {
            return;
        }
 
        // It's ok to not have a rootnamespace property, but if it's there we want to use it correctly
        context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(MatchFolderAndNamespaceConstants.RootNamespaceOption, out var rootNamespace);
 
        // Project directory is a must to correctly get the relative path and construct a namespace
        if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(MatchFolderAndNamespaceConstants.ProjectDirOption, out var projectDir)
            || string.IsNullOrEmpty(projectDir))
        {
            return;
        }
 
        var namespaceDecl = (TNamespaceSyntax)context.Node;
        var symbol = context.SemanticModel.GetDeclaredSymbol(namespaceDecl);
        RoslynDebug.AssertNotNull(symbol);
 
        var currentNamespace = symbol.ToDisplayString(s_namespaceDisplayFormat);
 
        if (IsFileAndNamespaceMismatch(namespaceDecl, rootNamespace, projectDir, currentNamespace, out var targetNamespace) &&
            IsFixSupported(context.SemanticModel, namespaceDecl, context.CancellationToken))
        {
            var nameSyntax = GetSyntaxFacts().GetNameOfBaseNamespaceDeclaration(namespaceDecl);
            RoslynDebug.AssertNotNull(nameSyntax);
 
            context.ReportDiagnostic(DiagnosticHelper.Create(
                Descriptor,
                nameSyntax.GetLocation(),
                option.Notification,
                context.Options,
                additionalLocations: null,
                properties: ImmutableDictionary<string, string?>.Empty.Add(MatchFolderAndNamespaceConstants.TargetNamespace, targetNamespace),
                messageArgs: new[] { currentNamespace, targetNamespace }));
        }
    }
 
    private bool IsFixSupported(SemanticModel semanticModel, TNamespaceSyntax namespaceDeclaration, CancellationToken cancellationToken)
    {
        var root = namespaceDeclaration.SyntaxTree.GetRoot(cancellationToken);
 
        // It should not be nested in other namespaces
        if (namespaceDeclaration.Ancestors().OfType<TNamespaceSyntax>().Any())
        {
            return false;
        }
 
        // It should not contain a namespace
        var containsNamespace = namespaceDeclaration
             .DescendantNodes(n => n is TNamespaceSyntax)
             .OfType<TNamespaceSyntax>().Any();
        if (containsNamespace)
        {
            return false;
        }
 
        // The current namespace should be valid
        var isCurrentNamespaceInvalid = GetSyntaxFacts()
            .GetNameOfBaseNamespaceDeclaration(namespaceDeclaration)
            ?.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)
            ?? false;
 
        if (isCurrentNamespaceInvalid)
        {
            return false;
        }
 
        // It should not contain partial classes with more than one instance in the semantic model. The
        // fixer does not support this scenario.
        var containsPartialType = ContainsPartialTypeWithMultipleDeclarations(namespaceDeclaration, semanticModel);
        if (containsPartialType)
        {
            return false;
        }
 
        return true;
    }
 
    private bool IsFileAndNamespaceMismatch(
        TNamespaceSyntax namespaceDeclaration,
        string? rootNamespace,
        string projectDir,
        string currentNamespace,
        [NotNullWhen(returnValue: true)] out string? targetNamespace)
    {
        if (!PathUtilities.IsChildPath(projectDir, namespaceDeclaration.SyntaxTree.FilePath))
        {
            // The file does not exist within the project directory
            targetNamespace = null;
            return false;
        }
 
        var relativeDirectoryPath = PathUtilities.GetRelativePath(
            projectDir,
            PathUtilities.GetDirectoryName(namespaceDeclaration.SyntaxTree.FilePath)!);
        var folders = relativeDirectoryPath.Split([Path.DirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries);
 
        var expectedNamespace = PathMetadataUtilities.TryBuildNamespaceFromFolders(folders, GetSyntaxFacts(), rootNamespace);
 
        if (RoslynString.IsNullOrWhiteSpace(expectedNamespace) || expectedNamespace.Equals(currentNamespace, StringComparison.OrdinalIgnoreCase))
        {
            // The namespace currently matches the folder structure or is invalid, in which case we don't want
            // to provide a diagnostic.
            targetNamespace = null;
            return false;
        }
 
        targetNamespace = expectedNamespace;
        return true;
    }
 
    /// <summary>
    /// Returns true if the namespace declaration contains one or more partial types with multiple declarations.
    /// </summary>
    protected bool ContainsPartialTypeWithMultipleDeclarations(TNamespaceSyntax namespaceDeclaration, SemanticModel semanticModel)
    {
        var syntaxFacts = GetSyntaxFacts();
 
        var typeDeclarations = syntaxFacts.GetMembersOfBaseNamespaceDeclaration(namespaceDeclaration)
            .Where(syntaxFacts.IsTypeDeclaration);
 
        foreach (var typeDecl in typeDeclarations)
        {
            var symbol = semanticModel.GetDeclaredSymbol(typeDecl);
 
            // Simplify the check by assuming no multiple partial declarations in one document
            if (symbol is ITypeSymbol typeSymbol && typeSymbol.DeclaringSyntaxReferences.Length > 1)
            {
                return true;
            }
        }
 
        return false;
    }
}