mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* Add ValidateMemberAttribute, analyzer and test
* Use attribute on DirtyFields methods
* Defer member lookup
* Additional test case
* Add support for collection types
* Poke tests
* Revert "Add support for collection types"
This reverts commit 2b8f5534bd.
* break, not continue
* Cheaper attribute check with AttributeHelper
* Clean up unused helper method
---------
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
96 lines
3.8 KiB
C#
96 lines
3.8 KiB
C#
#nullable enable
|
|
using System.Collections.Immutable;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Diagnostics;
|
|
using Microsoft.CodeAnalysis.Operations;
|
|
using Robust.Roslyn.Shared;
|
|
|
|
namespace Robust.Analyzers;
|
|
|
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer
|
|
{
|
|
private const string ValidateMemberType = "Robust.Shared.Analyzers.ValidateMemberAttribute";
|
|
|
|
private static readonly DiagnosticDescriptor ValidateMemberDescriptor = new(
|
|
Diagnostics.IdValidateMember,
|
|
"Invalid member name",
|
|
"{0} is not a member of {1}",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Be sure the type and member name are correct.");
|
|
|
|
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ValidateMemberDescriptor];
|
|
|
|
public override void Initialize(AnalysisContext context)
|
|
{
|
|
context.EnableConcurrentExecution();
|
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
|
context.RegisterSyntaxNodeAction(AnalyzeExpression, SyntaxKind.InvocationExpression);
|
|
}
|
|
|
|
private void AnalyzeExpression(SyntaxNodeAnalysisContext context)
|
|
{
|
|
if (context.Node is not InvocationExpressionSyntax node)
|
|
return;
|
|
|
|
if (context.SemanticModel.GetSymbolInfo(node.Expression).Symbol is not IMethodSymbol methodSymbol)
|
|
return;
|
|
|
|
// We need at least one type argument for context
|
|
if (methodSymbol.TypeArguments.Length < 1)
|
|
return;
|
|
|
|
// We'll be checking members of the first type argument
|
|
if (methodSymbol.TypeArguments[0] is not INamedTypeSymbol targetType)
|
|
return;
|
|
|
|
// We defer building this set until we need it later, so we don't have to build it for every single method invocation!
|
|
ImmutableHashSet<ISymbol>? members = null;
|
|
|
|
// Check each parameter of the method
|
|
foreach (var parameterContext in node.ArgumentList.Arguments)
|
|
{
|
|
|
|
// Get the symbol for this parameter
|
|
if (context.SemanticModel.GetOperation(parameterContext) is not IArgumentOperation op || op.Parameter is null)
|
|
continue;
|
|
var parameterSymbol = op.Parameter.OriginalDefinition;
|
|
|
|
// Make sure the parameter has the ValidateMember attribute
|
|
if (!AttributeHelper.HasAttribute(parameterSymbol, ValidateMemberType, out _))
|
|
continue;
|
|
|
|
// Find the value passed for this parameter.
|
|
// We use GetConstantValue to resolve compile-time values - i.e. the result of nameof()
|
|
if (context.SemanticModel.GetConstantValue(parameterContext.Expression).Value is not string fieldName)
|
|
continue;
|
|
|
|
// Get a set containing all the members of the target type and its ancestors
|
|
members ??= targetType.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()).ToImmutableHashSet(SymbolEqualityComparer.Default);
|
|
|
|
// Check each member of the target type to see if it matches our passed in value
|
|
var found = false;
|
|
foreach (var member in members)
|
|
{
|
|
if (member.Name == fieldName)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
// If we didn't find it, report the violation
|
|
if (!found)
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
ValidateMemberDescriptor,
|
|
parameterContext.GetLocation(),
|
|
fieldName,
|
|
targetType.Name
|
|
));
|
|
}
|
|
}
|
|
}
|