Files
RobustToolbox/Robust.Analyzers/HasDependenciesCodeFixProvider.cs
b4eb85ad3c [Dependency] source generator (#6549)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Release notes

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:02 +02:00

115 lines
4.3 KiB
C#

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[ExportCodeFixProvider(LanguageNames.CSharp)]
public sealed class HasDependenciesCodeFixProvider : CodeFixProvider
{
private const string TitleRemoveReadonly = "Remove readonly from dependency";
private const string TitlePartial = "Add partial";
public override ImmutableArray<string> FixableDiagnosticIds =>
[
Diagnostics.IdHasDependenciesReadOnly, Diagnostics.IdHasDependenciesNotPartialParent,
Diagnostics.IdHasDependenciesNotPartial
];
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
foreach (var diagnostic in context.Diagnostics)
{
switch (diagnostic.Id)
{
case Diagnostics.IdHasDependenciesReadOnly:
{
var diagnosticSpan = diagnostic.Location.SourceSpan;
var declaration = root!.FindToken(diagnosticSpan.Start).Parent!.AncestorsAndSelf()
.OfType<FieldDeclarationSyntax>()
.First();
context.RegisterCodeFix(
CodeAction.Create(
TitleRemoveReadonly,
c => FixReadOnlyAsync(context.Document, declaration, c),
TitleRemoveReadonly),
diagnostic);
break;
}
case Diagnostics.IdHasDependenciesNotPartial:
case Diagnostics.IdHasDependenciesNotPartialParent:
{
var diagnosticSpan = diagnostic.Location.SourceSpan;
var declaration = root!.FindToken(diagnosticSpan.Start).Parent!.AncestorsAndSelf()
.OfType<TypeDeclarationSyntax>()
.First();
context.RegisterCodeFix(
CodeAction.Create(
TitlePartial,
c => FixPartialAsync(context.Document, declaration, c),
TitlePartial),
diagnostic);
break;
}
}
}
}
private static async Task<Document> FixReadOnlyAsync(
Document document,
FieldDeclarationSyntax origDeclaration,
CancellationToken cancel)
{
var readonlyModifier = origDeclaration.Modifiers.Single(m => m.IsKind(SyntaxKind.ReadOnlyKeyword));
var newDeclaration = origDeclaration.WithModifiers(origDeclaration.Modifiers.Remove(readonlyModifier));
var formattedField = newDeclaration.WithAdditionalAnnotations(Formatter.Annotation);
var oldRoot = await document.GetSyntaxRootAsync(cancel);
var newRoot = oldRoot!.ReplaceNode(origDeclaration, formattedField);
return document.WithSyntaxRoot(newRoot);
}
private static async Task<Document> FixPartialAsync(
Document document,
TypeDeclarationSyntax declaration,
CancellationToken cancel)
{
var root = (await document.GetSyntaxRootAsync(cancel))!;
var nodesToPartial = new List<SyntaxNode>();
do
{
if (!declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
nodesToPartial.Add(declaration);
declaration = declaration.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault();
} while (declaration != null);
root = root.TrackNodes(nodesToPartial.ToArray());
foreach (var node in nodesToPartial)
{
var old = (TypeDeclarationSyntax)root.GetCurrentNode(node)!;
root = root.ReplaceNode(old, old.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)));
}
return document.WithSyntaxRoot(root);
}
public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
}