Files
RobustToolbox/Robust.Analyzers/AccessAnalyzer.cs
Pieter-Jan Briers ae6cebbfbb Source gen reorganizations + component unpause generator. (#4896)
* Source gen reorganizations + component unpause generator.

This commit (and subsequent commits) aims to clean up our Roslyn plugin (source gens + analyzers) stack to more sanely re-use common code

I also built a new source-gen that automatically generates unpausing implementations for components, incrementing attributed TimeSpan field when unpaused.

* Fix warnings in all Roslyn projects
2024-02-20 10:15:32 +01:00

266 lines
10 KiB
C#

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
using Robust.Shared.Analyzers.Implementation;
namespace Robust.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AccessAnalyzer : DiagnosticAnalyzer
{
private const string AccessAttributeType = "Robust.Shared.Analyzers.AccessAttribute";
private const string RobustAutoGeneratedAttributeType = "Robust.Shared.Analyzers.RobustAutoGeneratedAttribute";
private const string PureAttributeType = "System.Diagnostics.Contracts.PureAttribute";
[SuppressMessage("ReSharper", "RS2008")]
private static readonly DiagnosticDescriptor AccessRule = new (
Diagnostics.IdAccess,
"Invalid access",
"Tried to perform {0} access to member '{1}' in type '{2}', despite {3} access. {4}.",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure to give the accessing type the correct access permissions.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(AccessRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(CheckFriendship,
OperationKind.FieldReference,
OperationKind.PropertyReference,
OperationKind.MethodReference,
OperationKind.Invocation);
}
private void CheckFriendship(OperationAnalysisContext context)
{
var operation = context.Operation;
// The symbol representing the member being accessed.
ISymbol member;
// The operation to target when determining access type.
IOperation targetAccess;
switch (operation)
{
case IMemberReferenceOperation memberRef:
{
member = memberRef.Member;
targetAccess = memberRef.Parent;
break;
}
case IInvocationOperation invocation:
{
member = invocation.TargetMethod;
targetAccess = invocation;
break;
}
default:
return;
}
// Get the info of the type defining the member, so we can check the attributes later...
var accessedType = member.ContainingType;
// Get the attributes
var friendAttribute = context.Compilation.GetTypeByMetadataName(AccessAttributeType);
var autoGenAttribute = context.Compilation.GetTypeByMetadataName(RobustAutoGeneratedAttributeType);
// Get the type that is containing this expression, or, the type where this is happening.
if (context.ContainingSymbol?.ContainingType is not {} accessingType)
return;
// Should we ignore the access attempt due to the accessing type being auto-generated?
if (accessingType.GetAttributes().FirstOrDefault(a =>
a.AttributeClass != null &&
a.AttributeClass.Equals(autoGenAttribute, SymbolEqualityComparer.Default)) is { } attr)
{
return;
}
// Determine which type of access is happening here... Read, write or execute?
var accessAttempt = DetermineAccess(context, targetAccess, operation);
// Check whether this is a "self" access, including inheritors.
var selfAccess = InheritsFromOrEquals(accessingType, accessedType);
// Helper function to deduplicate attribute-checking code.
bool CheckAttributeFriendship(AttributeData attribute, bool isMemberAttribute)
{
// If the attribute isn't the friend attribute, we don't care about it.
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, friendAttribute))
return false;
var self = AccessAttribute.SelfDefaultPermissions;
var friends = AccessAttribute.FriendDefaultPermissions;
var others = AccessAttribute.OtherDefaultPermissions;
foreach (var kv in attribute.NamedArguments)
{
if (kv.Value.Value is not byte value)
continue;
var permissions = (AccessPermissions) value;
switch (kv.Key)
{
case nameof(AccessAttribute.Self):
{
self = permissions;
break;
}
case nameof(AccessAttribute.Friend):
{
friends = permissions;
break;
}
case nameof(AccessAttribute.Other):
{
others = permissions;
break;
}
default:
continue;
}
}
// By default, we will check the "other" permissions unless we find we're dealing with a friend or self.
var permissionCheck = others;
// Human-readable relation between accessing and accessed types.
var accessingRelation = "other-type";
if (!selfAccess)
{
// This is not a self-access, so we need to determine whether the accessing type is a friend.
// Check all types allowed in the friend attribute. (We assume there's only one constructor arg.)
var types = attribute.ConstructorArguments[0].Values;
foreach (var constant in types)
{
// Check if the value is a type...
if (constant.Value is not INamedTypeSymbol friendType)
continue;
// Check if the accessing type is specified in the attribute...
if (!InheritsFromOrEquals(accessingType, friendType))
continue;
// Set the permissions check to the friend permissions!
permissionCheck = friends;
accessingRelation = "friend-type";
break;
}
}
else
{
// Self-access, so simply set the permissions check to self.
permissionCheck = self;
accessingRelation = "same-type";
}
// If we allow this access, return! All is good.
if ((accessAttempt & permissionCheck) != 0)
return true;
// Access denied! Report an error.
context.ReportDiagnostic(
Diagnostic.Create(AccessRule, operation.Syntax.GetLocation(),
$"a{(accessAttempt == AccessPermissions.Execute ? "n" : "")} '{accessAttempt}' {accessingRelation}",
$"{member.Name}",
$"{accessedType.Name}",
$"{(permissionCheck == AccessPermissions.None ? "having no" : $"only having '{permissionCheck}'")}",
$"{(isMemberAttribute ? "Member" : "Type")} Permissions: {self.ToUnixPermissions()}{friends.ToUnixPermissions()}{others.ToUnixPermissions()}"));
// Only return ONE error.
return true;
}
// Check attributes in the member first, since they take priority and can override type restrictions.
foreach (var attribute in member.GetAttributes())
{
if(CheckAttributeFriendship(attribute, true))
return;
}
// Check attributes in the type containing the member last.
foreach (var attribute in accessedType.GetAttributes())
{
if(CheckAttributeFriendship(attribute, false))
return;
}
}
private static AccessPermissions DetermineAccess(OperationAnalysisContext context, IOperation operation, IOperation original)
{
switch (operation)
{
case IAssignmentOperation assign:
{
return assign.Target.Equals(original) ? AccessPermissions.Write : AccessPermissions.Read;
}
case IInvocationOperation invoke:
{
var pureAttribute = context.Compilation.GetTypeByMetadataName(PureAttributeType);
foreach (var attribute in invoke.TargetMethod.GetAttributes())
{
// Pure methods are treated as read accesses.
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, pureAttribute))
return AccessPermissions.Read;
}
return AccessPermissions.Execute;
}
case IMemberReferenceOperation member:
{
return DetermineAccess(context, member.Parent, operation);
}
default:
{
return AccessPermissions.Read;
}
}
}
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;
}
}
}
}