|
// 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;
}
}
|