mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 23:02:34 +02:00
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
130 lines
5.1 KiB
C#
130 lines
5.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Diagnostics;
|
|
|
|
namespace Robust.Analyzers
|
|
{
|
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
public class FriendAnalyzer : DiagnosticAnalyzer
|
|
{
|
|
const string FriendAttribute = "Robust.Shared.Analyzers.FriendAttribute";
|
|
|
|
[SuppressMessage("ReSharper", "RS2008")]
|
|
private static readonly DiagnosticDescriptor Rule = new (
|
|
Diagnostics.IdFriend,
|
|
"Tried to access friend-only member",
|
|
"Tried to access member \"{0}\" in class \"{1}\" which can only be accessed by friend classes",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Make sure to specify the accessing class in the friends attribute.");
|
|
|
|
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
|
|
|
public override void Initialize(AnalysisContext context)
|
|
{
|
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
|
context.EnableConcurrentExecution();
|
|
context.RegisterSyntaxNodeAction(CheckFriendship, SyntaxKind.SimpleMemberAccessExpression);
|
|
}
|
|
|
|
private void CheckFriendship(SyntaxNodeAnalysisContext context)
|
|
{
|
|
if (context.Node is not MemberAccessExpressionSyntax memberAccess)
|
|
return;
|
|
|
|
// We only do something if our parent is one of a few types.
|
|
switch (context.Node.Parent)
|
|
{
|
|
// If we're being assigned...
|
|
case AssignmentExpressionSyntax assignParent:
|
|
{
|
|
if (assignParent.Left != memberAccess)
|
|
return;
|
|
break;
|
|
}
|
|
|
|
// If we're being invoked...
|
|
case InvocationExpressionSyntax:
|
|
break;
|
|
|
|
// Otherwise, do nothing.
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// Get the friend attribute
|
|
var friendAttr = context.Compilation.GetTypeByMetadataName(FriendAttribute);
|
|
|
|
// Get the type that is containing this expression, or, the class where this is happening.
|
|
if (context.ContainingSymbol?.ContainingType is not { } containingType)
|
|
return;
|
|
|
|
// We check all of our children and get only the identifiers.
|
|
foreach (var identifier in memberAccess.ChildNodes().Select(node => node as IdentifierNameSyntax))
|
|
{
|
|
if (identifier == null) continue;
|
|
|
|
// Get the type info of the identifier, so we can check the attributes...
|
|
if (context.SemanticModel.GetTypeInfo(identifier).ConvertedType is not { } type)
|
|
continue;
|
|
|
|
// Same-type access is always fine.
|
|
if (SymbolEqualityComparer.Default.Equals(type, containingType))
|
|
continue;
|
|
|
|
// Finally, get all attributes of the type, to check if we have any friend classes.
|
|
foreach (var attribute in type.GetAttributes())
|
|
{
|
|
// If the attribute isn't the friend attribute, continue.
|
|
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, friendAttr))
|
|
continue;
|
|
|
|
// Check all types allowed in the friend attribute. (We assume there's only one constructor arg.)
|
|
foreach (var constant in attribute.ConstructorArguments[0].Values)
|
|
{
|
|
// Check if the value is a type...
|
|
if (constant.Value is not INamedTypeSymbol t)
|
|
continue;
|
|
|
|
// If we find that the containing class is specified in the attribute, return! All is good.
|
|
if (InheritsFromOrEquals(containingType, t))
|
|
return;
|
|
}
|
|
|
|
// Not in a friend class! Report an error.
|
|
context.ReportDiagnostic(
|
|
Diagnostic.Create(Rule, context.Node.GetLocation(),
|
|
$"{context.Node.ToString().Split('.').LastOrDefault()}", $"{type.Name}"));
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool InheritsFromOrEquals(INamedTypeSymbol type, INamedTypeSymbol baseType)
|
|
{
|
|
foreach (var otherType in GetBaseTypesAndThis(type))
|
|
{
|
|
if (SymbolEqualityComparer.Default.Equals(otherType, baseType))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private IEnumerable<INamedTypeSymbol> GetBaseTypesAndThis(INamedTypeSymbol namedType)
|
|
{
|
|
var current = namedType;
|
|
while (current != null)
|
|
{
|
|
yield return current;
|
|
current = current.BaseType;
|
|
}
|
|
}
|
|
}
|
|
}
|