mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-07 16:36:52 +02:00
* [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>
145 lines
5.4 KiB
C#
145 lines
5.4 KiB
C#
using System.Collections.Immutable;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Diagnostics;
|
|
using Robust.Roslyn.Shared;
|
|
|
|
namespace Robust.Analyzers;
|
|
|
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
public sealed class HasDependenciesAnalyzer : DiagnosticAnalyzer
|
|
{
|
|
private const string DependencyAttributeName = "Robust.Shared.IoC.DependencyAttribute";
|
|
|
|
public static readonly DiagnosticDescriptor DiagnosticNotPartial = new(
|
|
Diagnostics.IdHasDependenciesNotPartial,
|
|
"Type has dependencies but is not partial",
|
|
"Type '{0}' has [Dependency] fields but is not partial. This will be required in the future.",
|
|
"Usage",
|
|
DiagnosticSeverity.Warning,
|
|
true);
|
|
|
|
public static readonly DiagnosticDescriptor DiagnosticNotPartialParent = new(
|
|
Diagnostics.IdHasDependenciesNotPartialParent,
|
|
"Type has dependencies but is not in a partial type",
|
|
"Type '{0}' has [Dependency] fields but is nested in a non-partial type. The parent being partial will be required in the future.",
|
|
"Usage",
|
|
DiagnosticSeverity.Warning,
|
|
true);
|
|
|
|
public static readonly DiagnosticDescriptor DiagnosticReadOnly = new(
|
|
Diagnostics.IdHasDependenciesReadOnly,
|
|
"Dependency field is readonly",
|
|
"Field '{0}' is a [Dependency] but is readonly. This will be an error in the future.",
|
|
"Usage",
|
|
DiagnosticSeverity.Warning,
|
|
true);
|
|
|
|
public static readonly DiagnosticDescriptor DiagnosticPropertyField = new(
|
|
Diagnostics.IdHasDependenciesPropertyField,
|
|
"Property backing fields cannot be a dependency",
|
|
"Property '{0}' has a backing field marked with [Dependency]. This will be an error in the future.",
|
|
"Usage",
|
|
DiagnosticSeverity.Warning,
|
|
true);
|
|
|
|
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
|
|
[DiagnosticNotPartial, DiagnosticNotPartialParent, DiagnosticReadOnly, DiagnosticPropertyField];
|
|
|
|
public override void Initialize(AnalysisContext context)
|
|
{
|
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
|
context.EnableConcurrentExecution();
|
|
context.RegisterCompilationStartAction(static ctx =>
|
|
{
|
|
var attr = ctx.Compilation.GetTypeByMetadataName(DependencyAttributeName);
|
|
if (attr == null)
|
|
return;
|
|
|
|
ctx.RegisterSymbolAction(ctx => AnalyzeType(ctx, attr), SymbolKind.NamedType);
|
|
});
|
|
}
|
|
|
|
private static void AnalyzeType(SymbolAnalysisContext ctx, INamedTypeSymbol dependencyAttr)
|
|
{
|
|
if (ctx.Symbol is not INamedTypeSymbol typeSymbol)
|
|
return;
|
|
|
|
var hasDependencies = false;
|
|
foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>())
|
|
{
|
|
if (!AttributeHelper.HasAttribute(fieldSymbol, dependencyAttr, out _))
|
|
continue;
|
|
|
|
hasDependencies = true;
|
|
|
|
if (!fieldSymbol.CanBeReferencedByName)
|
|
{
|
|
if (fieldSymbol.AssociatedSymbol is IPropertySymbol prop)
|
|
{
|
|
// I wanted to make this work, but ForAttributeWithMetadataName doesn't work with
|
|
// backing field attributes.
|
|
// https://github.com/dotnet/roslyn/issues/80511
|
|
// Putting [Dependency] on the property directly isn't backwards-compatible with the old system.
|
|
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticPropertyField,
|
|
prop.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(),
|
|
prop.Name));
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (fieldSymbol.IsReadOnly)
|
|
{
|
|
var fieldSyntax =
|
|
(FieldDeclarationSyntax)fieldSymbol.DeclaringSyntaxReferences[0].GetSyntax().Parent!.Parent!;
|
|
foreach (var modifier in fieldSyntax.Modifiers)
|
|
{
|
|
if (modifier.IsKind(SyntaxKind.ReadOnlyKeyword))
|
|
{
|
|
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticReadOnly,
|
|
modifier.GetLocation(),
|
|
fieldSymbol.Name));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasDependencies)
|
|
return;
|
|
|
|
var origSyntax = (TypeDeclarationSyntax)typeSymbol.DeclaringSyntaxReferences[0].GetSyntax();
|
|
var syntax = origSyntax;
|
|
|
|
while (syntax != null)
|
|
{
|
|
var foundPartial = false;
|
|
foreach (var modifier in syntax.Modifiers)
|
|
{
|
|
if (modifier.IsKind(SyntaxKind.PartialKeyword))
|
|
{
|
|
foundPartial = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundPartial)
|
|
{
|
|
var diag = syntax == origSyntax
|
|
? DiagnosticNotPartial
|
|
: DiagnosticNotPartialParent;
|
|
|
|
ctx.ReportDiagnostic(Diagnostic.Create(diag,
|
|
origSyntax.Keyword.GetLocation(),
|
|
typeSymbol.ToDisplayString()));
|
|
|
|
break;
|
|
}
|
|
|
|
syntax = syntax.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault();
|
|
}
|
|
}
|
|
}
|