Skip to content

Instantly share code, notes, and snippets.

@OliverVea
Created August 28, 2024 07:39
Show Gist options
  • Select an option

  • Save OliverVea/832e2be7b82c2c9e4d886b0326498b88 to your computer and use it in GitHub Desktop.

Select an option

Save OliverVea/832e2be7b82c2c9e4d886b0326498b88 to your computer and use it in GitHub Desktop.
Attribute for limiting use of classes and methods across namespaces
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