File: src\roslyn\src\Analyzers\CSharp\Analyzers\MisplacedUsingDirectives\MisplacedUsingDirectivesDiagnosticAnalyzer.cs
Web Access
Project: src\roslyn\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// 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.Linq;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeAnalysis.CSharp.MisplacedUsingDirectives;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class MisplacedUsingDirectivesDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
    private static readonly LocalizableResourceString s_localizableTitle = new(
       nameof(CSharpAnalyzersResources.Misplaced_using_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources));

    private static readonly LocalizableResourceString s_localizableOutsideMessage = new(
        nameof(CSharpAnalyzersResources.Using_directives_must_be_placed_outside_of_a_namespace_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources));

    private static readonly DiagnosticDescriptor s_outsideDiagnosticDescriptor = CreateDescriptorWithId(
        IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId,
        EnforceOnBuildValues.MoveMisplacedUsingDirectives,
        hasAnyCodeStyleOption: true,
        s_localizableTitle, s_localizableOutsideMessage);

    private static readonly LocalizableResourceString s_localizableInsideMessage = new(
        nameof(CSharpAnalyzersResources.Using_directives_must_be_placed_inside_of_a_namespace_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources));

    private static readonly DiagnosticDescriptor s_insideDiagnosticDescriptor = CreateDescriptorWithId(
        IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId,
        EnforceOnBuildValues.MoveMisplacedUsingDirectives,
        hasAnyCodeStyleOption: true,
        s_localizableTitle, s_localizableInsideMessage);

    public MisplacedUsingDirectivesDiagnosticAnalyzer()
       : base(
            [
                (s_outsideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement),
                (s_insideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement)
            ])
    {
    }

    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis;

    protected override void InitializeWorker(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, SyntaxKind.NamespaceDeclaration, SyntaxKind.FileScopedNamespaceDeclaration);
        context.RegisterSyntaxNodeAction(AnalyzeCompilationUnitNode, SyntaxKind.CompilationUnit);
    }

    private void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context)
    {
        var option = context.GetCSharpAnalyzerOptions().UsingDirectivePlacement;
        if (option.Value == AddImportPlacement.InsideNamespace
            || ShouldSkipAnalysis(context, option.Notification))
        {
            return;
        }

        var namespaceDeclaration = (BaseNamespaceDeclarationSyntax)context.Node;
        ReportDiagnostics(
            context,
            s_outsideDiagnosticDescriptor,
            // Move all usings outside of the namespace.  Ignore using-aliases if the user has set the option saying
            // that they're ok with them inside a namespace.
            option.Value is AddImportPlacement.OutsideNamespaceIgnoringAliases
                ? namespaceDeclaration.Usings.Where(u => u.Alias is null)
                : namespaceDeclaration.Usings,
            option);
    }

    private void AnalyzeCompilationUnitNode(SyntaxNodeAnalysisContext context)
    {
        var option = context.GetCSharpAnalyzerOptions().UsingDirectivePlacement;
        var compilationUnit = (CompilationUnitSyntax)context.Node;

        if (option.Value != AddImportPlacement.InsideNamespace
           || ShouldSkipAnalysis(context, option.Notification)
           || ShouldSuppressDiagnostic(compilationUnit))
        {
            return;
        }

        // Only report for non-global usings.  Global usings must stay at the compilation unit level.
        var nonGlobalUsings = compilationUnit.Usings.Where(u => u.GlobalKeyword == default);

        // Note: We will report diagnostics when a code file contains multiple namespaces even though we will not
        // offer a code fix in these cases.
        ReportDiagnostics(context, s_insideDiagnosticDescriptor, nonGlobalUsings, option);
    }

    private static bool ShouldSuppressDiagnostic(CompilationUnitSyntax compilationUnit)
    {
        // Suppress if there are nodes other than usings and namespaces in the 
        // compilation unit (including ExternAlias).
        return compilationUnit.ChildNodes().Any(
            t => t.Kind() is not (SyntaxKind.UsingDirective or SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration));
    }

    private static void ReportDiagnostics(
       SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor,
       IEnumerable<UsingDirectiveSyntax> usingDirectives, CodeStyleOption2<AddImportPlacement> option)
    {
        foreach (var usingDirective in usingDirectives)
        {
            context.ReportDiagnostic(DiagnosticHelper.Create(
                descriptor,
                usingDirective.GetLocation(),
                option.Notification,
                context.Options,
                additionalLocations: null,
                properties: null));
        }
    }
}