Files
RobustToolbox/Robust.Analyzers/PrototypeFixer.cs
Tayrtahn c1737a540f Analyzer & Fixer for redundant Prototype type strings (#5718)
* Add Prototype analyzer

* Add Prototype fixer

* Early return after finding prototype attribute

* Add PrototypeEndsWithPrototypeRule diagnostic

* Oops. Uncomment parallelizable.

* Rework to ignore redundancy for non-literal string values

* Allow redundancy when removal would expose class name not ending in "Prototype"

* Promote PrototypeEndsWithPrototypeRule from warning to error, since it causes a runtime error.

* No need to get the symbol to get the class identifier

* Minor cleanup

* A little more cleanup

* More specific location for redundant name

* Refactor redundant name fixer so argument order is no longer important

* Add failing test

* Use symbol analysis to fix alias handling

* Oops! We have to go back to the previous syntax-based approach.

Now it's a hybrid.

Also fixed tests to not copy the prototype definitions.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-12-17 18:15:32 +01:00

78 lines
2.7 KiB
C#

#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Robust.Roslyn.Shared.Diagnostics;
namespace Robust.Analyzers;
[ExportCodeFixProvider(LanguageNames.CSharp)]
public sealed class PrototypeFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => [IdPrototypeRedundantType];
public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
foreach (var diagnostic in context.Diagnostics)
{
switch (diagnostic.Id)
{
case IdPrototypeRedundantType:
return RegisterRemoveType(context, diagnostic);
}
}
return Task.CompletedTask;
}
private static async Task RegisterRemoveType(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<AttributeArgumentSyntax>().First();
if (token == null)
return;
context.RegisterCodeFix(CodeAction.Create(
"Remove explicitly set type",
c => RemoveType(context.Document, token, c),
"Remove explicitly set type"
), diagnostic);
}
private static async Task<Document> RemoveType(Document document, AttributeArgumentSyntax syntax, CancellationToken cancellation)
{
var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation);
if (syntax.Parent is not AttributeArgumentListSyntax argListSyntax)
return document;
if (argListSyntax.Arguments.Count == 1)
{
// If this is the only argument, remove the whole argument list so we don't leave empty parentheses
if (argListSyntax.Parent is not AttributeSyntax attributeSyntax)
return document;
var newAttributeSyntax = attributeSyntax.RemoveNode(argListSyntax, SyntaxRemoveOptions.KeepNoTrivia);
root = root!.ReplaceNode(attributeSyntax, newAttributeSyntax!);
}
else
{
// Otherwise just remove the argument
var newArgListSyntax = argListSyntax.WithArguments(argListSyntax.Arguments.Remove(syntax));
root = root!.ReplaceNode(argListSyntax, newArgListSyntax);
}
return document.WithSyntaxRoot(root);
}
}