mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 14:52:35 +02:00
* Add source generator for DataDefinition validation * Add source generator for reading * Add source generator for writing * Include prototypes and other meansdatadefinition types * Target ISerializationGenerated in data definitions * Murder * Use array, struct, enum methods * Source generate get field definitions * serv5 * Fix and pray * Fix release compile * Tayrtahn review * a * Generator bugfix --------- Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
412 lines
16 KiB
C#
412 lines
16 KiB
C#
#nullable enable
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Diagnostics;
|
|
using Robust.Roslyn.Shared;
|
|
using Robust.Roslyn.Shared.Helpers;
|
|
using Robust.Shared.Serialization.Manager.Definition;
|
|
using Robust.Shared.ViewVariables;
|
|
|
|
namespace Robust.Analyzers;
|
|
|
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
|
{
|
|
private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute";
|
|
private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute";
|
|
private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute";
|
|
private const string DataFieldAttributeName = "DataField";
|
|
private const string ViewVariablesAttributeName = "ViewVariables";
|
|
|
|
public static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
|
|
Diagnostics.IdDataDefinitionPartial,
|
|
"Type must be partial",
|
|
"Type {0} is a DataDefinition but is not partial",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Make sure to mark any type that is a data definition as partial."
|
|
);
|
|
|
|
public static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
|
|
Diagnostics.IdNestedDataDefinitionPartial,
|
|
"Type must be partial",
|
|
"Type {0} contains nested data definition {1} but is not partial",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Make sure to mark any type containing a nested data definition as partial."
|
|
);
|
|
|
|
public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
|
|
Diagnostics.IdDataFieldPropertyWritable,
|
|
"Data field property must have a setter",
|
|
"Data field property {0} in data definition {1} does not have a setter",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Make sure to add a setter."
|
|
);
|
|
|
|
public static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
|
|
Diagnostics.IdDataFieldRedundantTag,
|
|
"Data field has redundant tag specified",
|
|
"Data field {0} in data definition {1} has an explicitly set tag that matches autogenerated tag",
|
|
"Usage",
|
|
DiagnosticSeverity.Info,
|
|
true,
|
|
"Make sure to remove the tag string from the data field attribute."
|
|
);
|
|
|
|
public static readonly DiagnosticDescriptor DataFieldNoVVReadWriteRule = new(
|
|
Diagnostics.IdDataFieldNoVVReadWrite,
|
|
"Data field has VV ReadWrite",
|
|
"Data field {0} in data definition {1} has ViewVariables attribute with ReadWrite access, which is redundant",
|
|
"Usage",
|
|
DiagnosticSeverity.Info,
|
|
true,
|
|
"Make sure to remove the ViewVariables attribute."
|
|
);
|
|
|
|
public static readonly DiagnosticDescriptor DataFieldYamlSerializableRule = new(
|
|
Diagnostics.IdDataFieldYamlSerializable,
|
|
"Data field type is not YAML serializable",
|
|
"Data field {0} in data definition {1} is type {2}, which is not YAML serializable",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Make sure to use a type that is YAML serializable."
|
|
);
|
|
|
|
public static readonly DiagnosticDescriptor DataFieldOutsideDefinition = new(
|
|
Diagnostics.IdDataFieldOutsideDefinition,
|
|
"Data field defined in a type that is not marked as a data definition or data record",
|
|
"Data field {0} is defined in type {1} which is not a data definition or data record",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Make sure to add a data definition or data record attribute, or inherit a type or add an attribute that implicitly makes its inheritors data definitions or data records."
|
|
);
|
|
|
|
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
|
DataDefinitionPartialRule,
|
|
NestedDataDefinitionPartialRule,
|
|
DataFieldPropertyWritableRule,
|
|
DataFieldRedundantTagRule,
|
|
DataFieldNoVVReadWriteRule,
|
|
DataFieldYamlSerializableRule,
|
|
DataFieldOutsideDefinition
|
|
);
|
|
|
|
public override void Initialize(AnalysisContext context)
|
|
{
|
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
|
|
context.EnableConcurrentExecution();
|
|
|
|
context.RegisterSymbolStartAction(symbolContext =>
|
|
{
|
|
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol)
|
|
return;
|
|
|
|
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
|
|
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
|
|
|
|
if (!IsDataDefinition(typeSymbol))
|
|
return;
|
|
|
|
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
|
|
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
|
|
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
|
|
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
|
|
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
|
|
}, SymbolKind.NamedType);
|
|
}
|
|
|
|
private static void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
|
|
{
|
|
if (context.Node is not TypeDeclarationSyntax declaration)
|
|
return;
|
|
|
|
if (context.ContainingSymbol is not INamedTypeSymbol type)
|
|
return;
|
|
|
|
if (!IsPartial(declaration))
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(DataDefinitionPartialRule, declaration.Keyword.GetLocation(), type.Name));
|
|
}
|
|
|
|
var containingType = type.ContainingType;
|
|
while (containingType != null)
|
|
{
|
|
var containingTypeDeclaration = (TypeDeclarationSyntax)containingType.DeclaringSyntaxReferences[0].GetSyntax();
|
|
if (!IsPartial(containingTypeDeclaration))
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(NestedDataDefinitionPartialRule, containingTypeDeclaration.Keyword.GetLocation(), containingType.Name, type.Name));
|
|
}
|
|
|
|
containingType = containingType.ContainingType;
|
|
}
|
|
}
|
|
|
|
private static void AnalyzeDataField(SyntaxNodeAnalysisContext context)
|
|
{
|
|
if (context.Node is not FieldDeclarationSyntax field)
|
|
return;
|
|
|
|
if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type)
|
|
return;
|
|
|
|
var isDataDefinition = DataDefinitionHelper.IsDataDefinition(type, out var isDataRecord);
|
|
foreach (var variable in field.Declaration.Variables)
|
|
{
|
|
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable);
|
|
|
|
if (fieldSymbol == null)
|
|
continue;
|
|
|
|
if (!DataDefinitionHelper.IsDataField(fieldSymbol, isDataRecord, out _, out var datafieldAttribute))
|
|
continue;
|
|
|
|
if (!isDataDefinition)
|
|
{
|
|
var location = type.DeclaringSyntaxReferences[0].GetSyntax().GetLocation();
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldOutsideDefinition, location, fieldSymbol.Name, type.Name));
|
|
}
|
|
|
|
if (HasRedundantTag(fieldSymbol, datafieldAttribute))
|
|
{
|
|
TryGetAttributeLocation(field, DataFieldAttributeName, out var location);
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, fieldSymbol.Name, type.Name));
|
|
}
|
|
|
|
if (HasVVReadWrite(fieldSymbol))
|
|
{
|
|
TryGetAttributeLocation(field, ViewVariablesAttributeName, out var location);
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, fieldSymbol.Name, type.Name));
|
|
}
|
|
|
|
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
|
|
continue;
|
|
|
|
fieldTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(fieldTypeSymbol);
|
|
|
|
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
|
(context.Node as FieldDeclarationSyntax)?.Declaration.Type.GetLocation(),
|
|
fieldSymbol.Name,
|
|
type.Name,
|
|
fieldTypeSymbol.MetadataName
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
|
|
{
|
|
if (context.Node is not PropertyDeclarationSyntax property)
|
|
return;
|
|
|
|
if (context.ContainingSymbol is not IPropertySymbol propertySymbol)
|
|
return;
|
|
|
|
if (propertySymbol.ContainingType is not INamedTypeSymbol type)
|
|
return;
|
|
|
|
if (propertySymbol == null)
|
|
return;
|
|
|
|
var isDataDefinition = DataDefinitionHelper.IsDataDefinition(type, out var isDataRecord);
|
|
if (isDataDefinition &&
|
|
!isDataRecord &&
|
|
propertySymbol.SetMethod == null &&
|
|
HasDataFieldAttribute(propertySymbol))
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
DataFieldPropertyWritableRule,
|
|
property.AccessorList?.GetLocation() ?? property.GetLocation(),
|
|
propertySymbol.Name,
|
|
type.Name));
|
|
return;
|
|
}
|
|
|
|
if (!DataDefinitionHelper.IsDataField(propertySymbol, isDataRecord, out _, out var datafieldAttribute))
|
|
return;
|
|
|
|
if (!isDataDefinition)
|
|
{
|
|
var location = type.DeclaringSyntaxReferences[0].GetSyntax().GetLocation();
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldOutsideDefinition, location, propertySymbol.Name, type.Name));
|
|
}
|
|
|
|
if (HasRedundantTag(propertySymbol, datafieldAttribute))
|
|
{
|
|
TryGetAttributeLocation(property, DataFieldAttributeName, out var location);
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, propertySymbol.Name, type.Name));
|
|
}
|
|
|
|
if (HasVVReadWrite(propertySymbol))
|
|
{
|
|
TryGetAttributeLocation(property, ViewVariablesAttributeName, out var location);
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, propertySymbol.Name, type.Name));
|
|
}
|
|
|
|
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
|
|
return;
|
|
|
|
propertyTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(propertyTypeSymbol);
|
|
|
|
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
|
(context.Node as PropertyDeclarationSyntax)?.Type.GetLocation(),
|
|
propertySymbol.Name,
|
|
type.Name,
|
|
propertyTypeSymbol.Name
|
|
));
|
|
}
|
|
}
|
|
|
|
private static bool HasDataFieldAttribute(ISymbol symbol)
|
|
{
|
|
foreach (var attribute in symbol.GetAttributes())
|
|
{
|
|
if (attribute.AttributeClass is { } attributeClass &&
|
|
TypeSymbolHelper.Inherits(attributeClass, DataFieldBaseNamespace))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsPartial(TypeDeclarationSyntax type)
|
|
{
|
|
return type.Modifiers.IndexOf(SyntaxKind.PartialKeyword) != -1;
|
|
}
|
|
|
|
private static bool IsDataDefinition(ITypeSymbol? type)
|
|
{
|
|
if (type == null)
|
|
return false;
|
|
|
|
return DataDefinitionHelper.IsDataDefinition(type, out _);
|
|
}
|
|
|
|
private static bool TryGetAttributeLocation(MemberDeclarationSyntax syntax, string attributeName, out Location location)
|
|
{
|
|
foreach (var attributeList in syntax.AttributeLists)
|
|
{
|
|
foreach (var attribute in attributeList.Attributes)
|
|
{
|
|
if (attribute.Name.ToString() != attributeName)
|
|
continue;
|
|
|
|
location = attribute.GetLocation();
|
|
return true;
|
|
}
|
|
}
|
|
// Default to the declaration syntax's location
|
|
location = syntax.GetLocation();
|
|
return false;
|
|
}
|
|
|
|
private static bool TryGetModifierLocation(MemberDeclarationSyntax syntax, SyntaxKind modifierKind, out Location location)
|
|
{
|
|
foreach (var modifier in syntax.Modifiers)
|
|
{
|
|
if (modifier.IsKind(modifierKind))
|
|
{
|
|
location = modifier.GetLocation();
|
|
return true;
|
|
}
|
|
}
|
|
location = syntax.GetLocation();
|
|
return false;
|
|
}
|
|
|
|
private static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member)
|
|
{
|
|
if (member is IFieldSymbol field)
|
|
{
|
|
return field.IsReadOnly;
|
|
}
|
|
else if (member is IPropertySymbol property)
|
|
{
|
|
if (property.SetMethod == null)
|
|
return true;
|
|
|
|
if (property.SetMethod.IsInitOnly)
|
|
return type.IsReferenceType;
|
|
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HasAttribute(ITypeSymbol type, string attributeName)
|
|
{
|
|
foreach (var attribute in type.GetAttributes())
|
|
{
|
|
if (attribute.AttributeClass?.ToDisplayString() == attributeName)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HasRedundantTag(ISymbol symbol, DataFieldAttribute datafieldAttribute)
|
|
{
|
|
// No args, no problem
|
|
if (datafieldAttribute.Data is not { ConstructorArguments.Length: > 0 })
|
|
return false;
|
|
|
|
// If a tag is explicitly specified, it will be the first argument...
|
|
var tagArgument = datafieldAttribute.Data.ConstructorArguments[0];
|
|
// ...but the first arg could also something else, since tag is optional
|
|
// so we make sure that it's a string
|
|
if (tagArgument.Value is not string explicitName)
|
|
return false;
|
|
|
|
// Get the name that sourcegen would provide
|
|
var automaticName = DataDefinitionUtility.AutoGenerateTag(symbol.Name);
|
|
|
|
// If the explicit name matches the sourcegen name, we have a redundancy
|
|
return explicitName == automaticName;
|
|
}
|
|
|
|
private static bool HasVVReadWrite(ISymbol symbol)
|
|
{
|
|
// Make sure it has ViewVariablesAttribute
|
|
AttributeData? viewVariablesAttribute = null;
|
|
foreach (var attr in symbol.GetAttributes())
|
|
{
|
|
if (attr.AttributeClass?.ToDisplayString() == ViewVariablesNamespace)
|
|
{
|
|
viewVariablesAttribute = attr;
|
|
}
|
|
}
|
|
if (viewVariablesAttribute == null)
|
|
return false;
|
|
|
|
// Default is ReadOnly, which is fine
|
|
if (viewVariablesAttribute.ConstructorArguments.Length == 0)
|
|
return false;
|
|
|
|
var accessArgument = viewVariablesAttribute.ConstructorArguments[0];
|
|
if (accessArgument.Value is not byte accessByte)
|
|
return false;
|
|
|
|
return (VVAccess)accessByte == VVAccess.ReadWrite;
|
|
}
|
|
|
|
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
|
|
{
|
|
return HasAttribute(type, NotYamlSerializableName);
|
|
}
|
|
}
|