Skip to content

Commit

Permalink
Add analyzer and fixer for redundant DataField tag arguments (#5134)
Browse files Browse the repository at this point in the history
* Add analyzer and fixer for redundant DataField tag arguments

* Share Tag autogeneration logic
  • Loading branch information
Tayrtahn authored May 17, 2024
1 parent b1329d3 commit da7abc6
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 7 deletions.
46 changes: 45 additions & 1 deletion Robust.Analyzers/DataDefinitionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
using Robust.Shared.Serialization.Manager.Definition;

namespace Robust.Analyzers;

Expand Down Expand Up @@ -56,8 +57,18 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to add a setter."
);

private 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 override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
DataFieldRedundantTagRule
);

public override void Initialize(AnalysisContext context)
Expand Down Expand Up @@ -125,6 +136,11 @@ private void AnalyzeDataField(SyntaxNodeAnalysisContext context)
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
}

if (HasRedundantTag(fieldSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
}
}
}

Expand All @@ -149,6 +165,11 @@ private void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
}

if (HasRedundantTag(propertySymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
}
}

private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
Expand Down Expand Up @@ -248,6 +269,29 @@ private static bool HasAttribute(ITypeSymbol type, string attributeName)
return false;
}

private static bool HasRedundantTag(ISymbol symbol)
{
if (!IsDataField(symbol, out var _, out var attribute))
return false;

// No args, no problem
if (attribute.ConstructorArguments.Length == 0)
return false;

// If a tag is explicitly specified, it will be the first argument...
var tagArgument = attribute.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 IsImplicitDataDefinition(ITypeSymbol type)
{
if (HasAttribute(type, ImplicitDataDefinitionNamespace))
Expand Down
72 changes: 68 additions & 4 deletions Robust.Analyzers/DataDefinitionFixer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#nullable enable
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
Expand All @@ -16,8 +13,11 @@ namespace Robust.Analyzers;
[ExportCodeFixProvider(LanguageNames.CSharp)]
public sealed class DefinitionFixer : CodeFixProvider
{
private const string DataFieldAttributeName = "DataField";

public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldWritable, IdDataFieldPropertyWritable
IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldWritable, IdDataFieldPropertyWritable,
IdDataFieldRedundantTag
);

public override Task RegisterCodeFixesAsync(CodeFixContext context)
Expand All @@ -34,6 +34,8 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
return RegisterDataFieldFix(context, diagnostic);
case IdDataFieldPropertyWritable:
return RegisterDataFieldPropertyFix(context, diagnostic);
case IdDataFieldRedundantTag:
return RegisterRedundantTagFix(context, diagnostic);
}
}

Expand Down Expand Up @@ -72,6 +74,68 @@ private static async Task<Document> MakeDataDefinitionPartial(Document document,
return document.WithSyntaxRoot(root);
}

private static async Task RegisterRedundantTagFix(CodeFixContext context, Diagnostic diagnostic)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
var span = diagnostic.Location.SourceSpan;
var token = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType<MemberDeclarationSyntax>().First();

if (token == null)
return;

// Find the DataField attribute
AttributeSyntax? dataFieldAttribute = null;
foreach (var attributeList in token.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
if (attribute.Name.ToString() == DataFieldAttributeName)
{
dataFieldAttribute = attribute;
break;
}
}
if (dataFieldAttribute != null)
break;
}

if (dataFieldAttribute == null)
return;

context.RegisterCodeFix(CodeAction.Create(
"Remove explicitly set tag",
c => RemoveRedundantTag(context.Document, dataFieldAttribute, c),
"Remove explicitly set tag"
), diagnostic);
}

private static async Task<Document> RemoveRedundantTag(Document document, AttributeSyntax syntax, CancellationToken cancellation)
{
var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation);

if (syntax.ArgumentList == null)
return document;

AttributeSyntax? newSyntax;
if (syntax.ArgumentList.Arguments.Count == 1)
{
// If this is the only argument, delete the ArgumentList so we don't leave empty parentheses
newSyntax = syntax.RemoveNode(syntax.ArgumentList, SyntaxRemoveOptions.KeepNoTrivia);
}
else
{
// Remove the first argument, which is the tag
var newArgs = syntax.ArgumentList.Arguments.RemoveAt(0);
var newArgList = syntax.ArgumentList.WithArguments(newArgs);
// Construct a new attribute with the tag removed
newSyntax = syntax.WithArgumentList(newArgList);
}

root = root!.ReplaceNode(syntax, newSyntax!);

return document.WithSyntaxRoot(root);
}

private static async Task RegisterDataFieldFix(CodeFixContext context, Diagnostic diagnostic)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
Expand Down
5 changes: 5 additions & 0 deletions Robust.Analyzers/Robust.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" LinkBase="Implementations" />
</ItemGroup>

<ItemGroup>
<!-- Needed for DataDefinitionAnalyzer. -->
<Compile Include="..\Robust.Shared\Serialization\Manager\Definition\DataDefinitionUtility.cs" LinkBase="Implementations" />
</ItemGroup>

<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />

<PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions Robust.Roslyn.Shared/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static class Diagnostics
public const string IdComponentPauseWrongTypeAttribute = "RA0024";
public const string IdDependencyFieldAssigned = "RA0025";
public const string IdUncachedRegex = "RA0026";
public const string IdDataFieldRedundantTag = "RA0027";

public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ internal DataDefinition(SerializationManager manager, bool isRecord)
continue;
}

var name = field.FieldInfo.Name.AsSpan();
attribute.Tag = $"{char.ToLowerInvariant(name[0])}{name[1..]}";
attribute.Tag = DataDefinitionUtility.AutoGenerateTag(field.FieldInfo.Name);
}

var dataFields = fieldDefs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Robust.Shared.Serialization.Manager.Definition;

public class DataDefinitionUtility
{
public static string AutoGenerateTag(string name)
{
var span = name.AsSpan();
return $"{char.ToLowerInvariant(span[0])}{span.Slice(1).ToString()}";
}
}

0 comments on commit da7abc6

Please sign in to comment.