Created
August 28, 2024 07:39
-
-
Save OliverVea/832e2be7b82c2c9e4d886b0326498b88 to your computer and use it in GitHub Desktop.
Attribute for limiting use of classes and methods across namespaces
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections.Immutable; | |
| using Microsoft.CodeAnalysis; | |
| using Microsoft.CodeAnalysis.CSharp.Syntax; | |
| using Microsoft.CodeAnalysis.Diagnostics; | |
| using DiagnosticDescriptor = Microsoft.CodeAnalysis.DiagnosticDescriptor; | |
| using DiagnosticSeverity = Microsoft.CodeAnalysis.DiagnosticSeverity; | |
| using LanguageNames = Microsoft.CodeAnalysis.LanguageNames; | |
| using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; | |
| namespace Gist; | |
| [Flags] | |
| public enum NamespaceTarget | |
| { | |
| None = 0, | |
| ThisNamespace = 1, | |
| ChildNamespaces = 2, | |
| ParentNamespaces = 4 | |
| } | |
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] | |
| public class ToBeUsedInAttribute(NamespaceTarget target = NamespaceTarget.ThisNamespace | NamespaceTarget.ChildNamespaces) : Attribute | |
| { | |
| public NamespaceTarget Target { get; } = target; | |
| } | |
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | |
| public class ToBeUsedInAnalyzer : DiagnosticAnalyzer | |
| { | |
| private const string DiagnosticId = "OHV0001"; | |
| private static readonly DiagnosticDescriptor Rule = new( | |
| DiagnosticId, | |
| "Namespace restriction violation", | |
| "Class, method, or property '{0}' is not allowed to be used in namespace '{1}', only in {2}", | |
| "Usage", | |
| DiagnosticSeverity.Error, | |
| isEnabledByDefault: true); | |
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | |
| public override void Initialize(AnalysisContext context) | |
| { | |
| context.EnableConcurrentExecution(); | |
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | |
| context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression, SyntaxKind.SimpleMemberAccessExpression); | |
| } | |
| private static void AnalyzeNode(SyntaxNodeAnalysisContext context) | |
| { | |
| var node = context.Node; | |
| var symbol = node switch | |
| { | |
| InvocationExpressionSyntax invocation => context.SemanticModel.GetSymbolInfo(invocation).Symbol, | |
| MemberAccessExpressionSyntax memberAccess => context.SemanticModel.GetSymbolInfo(memberAccess).Symbol, | |
| _ => null | |
| }; | |
| if (symbol is not IMethodSymbol and not IPropertySymbol) return; | |
| var declaringClass = symbol.ContainingType; | |
| var attributedClassNamespace = declaringClass.ContainingNamespace; | |
| var toBeUsedInSameNamespaceAttribute = declaringClass | |
| .GetAttributes() | |
| .FirstOrDefault(attr => attr.AttributeClass?.Name == nameof(ToBeUsedInAttribute)); | |
| if (toBeUsedInSameNamespaceAttribute is null) return; | |
| var namespaceTargetString = toBeUsedInSameNamespaceAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); | |
| if (string.IsNullOrEmpty(namespaceTargetString)) return; | |
| if (!Enum.TryParse<NamespaceTarget>(namespaceTargetString, out var namespaceTargets)) return; | |
| var model = context.SemanticModel; | |
| var classDeclaration = node.Ancestors().OfType<ClassDeclarationSyntax>().FirstOrDefault(); | |
| if (classDeclaration == null) return; | |
| var classSymbol = model.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol; | |
| var callingClassNamespace = classSymbol?.ContainingNamespace; | |
| if (callingClassNamespace == null) return; | |
| if (!IsNamespaceValid(callingClassNamespace, attributedClassNamespace, namespaceTargets)) | |
| { | |
| var diagnostic = Diagnostic.Create( | |
| Rule, | |
| node.GetLocation(), | |
| symbol.Name, | |
| callingClassNamespace.ToDisplayString(), | |
| AllowedNamespaceDescription(namespaceTargets, attributedClassNamespace.ToDisplayString())); | |
| context.ReportDiagnostic(diagnostic); | |
| } | |
| } | |
| private static bool IsNamespaceValid(INamespaceSymbol callingClassNamespace, INamespaceSymbol attributedClassNamespace, NamespaceTarget namespaceTargets) | |
| { | |
| var callingClassNamespaceString = callingClassNamespace.ToDisplayString(); | |
| var attributedClassNamespaceString = attributedClassNamespace.ToDisplayString(); | |
| return namespaceTargets.HasFlag(NamespaceTarget.ThisNamespace) && callingClassNamespaceString == attributedClassNamespaceString | |
| || namespaceTargets.HasFlag(NamespaceTarget.ChildNamespaces) && callingClassNamespaceString.StartsWith(attributedClassNamespaceString + ".") | |
| || namespaceTargets.HasFlag(NamespaceTarget.ParentNamespaces) && attributedClassNamespaceString.StartsWith(callingClassNamespaceString + "."); | |
| } | |
| private static string AllowedNamespaceDescription(NamespaceTarget namespaceTarget, string namespaceName) | |
| { | |
| var allowedTargets = new List<string>(); | |
| if (namespaceTarget.HasFlag(NamespaceTarget.ThisNamespace)) allowedTargets.Add($"'{namespaceName}'"); | |
| if (namespaceTarget.HasFlag(NamespaceTarget.ChildNamespaces)) allowedTargets.Add($"child namespaces of '{namespaceName}'"); | |
| if (namespaceTarget.HasFlag(NamespaceTarget.ParentNamespaces)) allowedTargets.Add($"parent namespaces of '{namespaceName}'"); | |
| switch (allowedTargets.Count) | |
| { | |
| case 1: | |
| return allowedTargets[0]; | |
| case 2: | |
| return string.Join(" and ", allowedTargets); | |
| default: | |
| { | |
| var commaSeparatedTargets = string.Join(", ", allowedTargets.Take(allowedTargets.Count - 1)); | |
| return $"{commaSeparatedTargets}, and {allowedTargets.Last()}"; | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment