mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* 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>
140 lines
4.9 KiB
C#
140 lines
4.9 KiB
C#
#nullable enable
|
|
using System.Collections.Immutable;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Diagnostics;
|
|
using Robust.Roslyn.Shared;
|
|
using Robust.Shared.Prototypes;
|
|
|
|
namespace Robust.Analyzers;
|
|
|
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
public sealed class PrototypeAnalyzer : DiagnosticAnalyzer
|
|
{
|
|
public static readonly DiagnosticDescriptor PrototypeRedundantTypeRule = new(
|
|
Diagnostics.IdPrototypeRedundantType,
|
|
"Redundant Prototype Type specification",
|
|
"Prototype {0} has explicitly set type \"{1}\" that matches autogenerated value",
|
|
"Usage",
|
|
DiagnosticSeverity.Warning,
|
|
true,
|
|
"Remove the redundant type specification."
|
|
);
|
|
|
|
public static readonly DiagnosticDescriptor PrototypeEndsWithPrototypeRule = new(
|
|
Diagnostics.IdPrototypeEndsWithPrototype,
|
|
"Prototype name must end with the word Prototype",
|
|
"Prototype {0} does not end with the word Prototype",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
true,
|
|
"Add the word Prototype to the end of the class name or manually specify a name in the Prototype attribute."
|
|
);
|
|
|
|
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
|
|
[PrototypeRedundantTypeRule, PrototypeEndsWithPrototypeRule];
|
|
|
|
public override void Initialize(AnalysisContext context)
|
|
{
|
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze |
|
|
GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
|
context.EnableConcurrentExecution();
|
|
|
|
context.RegisterCompilationStartAction(static ctx =>
|
|
{
|
|
var prototypeAttribute =
|
|
ctx.Compilation.GetTypeByMetadataName("Robust.Shared.Prototypes.PrototypeAttribute");
|
|
|
|
// No attribute, no analyzer.
|
|
if (prototypeAttribute is null)
|
|
return;
|
|
|
|
ctx.RegisterSyntaxNodeAction(
|
|
symCtx => AnalyzePrototype(symCtx, prototypeAttribute),
|
|
SyntaxKind.ClassDeclaration);
|
|
});
|
|
}
|
|
|
|
private static void AnalyzePrototype(SyntaxNodeAnalysisContext context, INamedTypeSymbol prototypeAttributeSymbol)
|
|
{
|
|
if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax)
|
|
return;
|
|
|
|
var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
|
|
if (classSymbol is null)
|
|
return;
|
|
|
|
var className = classSymbol.Name;
|
|
|
|
if (!AttributeHelper.HasAttribute(classSymbol, prototypeAttributeSymbol, out var attributeData))
|
|
return;
|
|
|
|
var prototypeAttribute = GetAttributeSyntax(attributeData, classDeclarationSyntax);
|
|
if (prototypeAttribute == null)
|
|
return;
|
|
|
|
// Check for autogenerated type
|
|
if (prototypeAttribute.ArgumentList?.Arguments[0] is not { } argumentSyntax)
|
|
{
|
|
if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding))
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(PrototypeEndsWithPrototypeRule,
|
|
classDeclarationSyntax.Identifier.GetLocation(),
|
|
className));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// We only care about redundancy if the argument is a string literal.
|
|
// Passing in a value that resolves to a redundant string is fine.
|
|
if (argumentSyntax.Expression is not LiteralExpressionSyntax literalSyntax)
|
|
return;
|
|
|
|
var literalValue = context.SemanticModel.GetConstantValue(literalSyntax);
|
|
if (literalValue.Value is not string specifiedName)
|
|
return;
|
|
|
|
var autoName = PrototypeUtility.CalculatePrototypeName(className);
|
|
|
|
// Check for name redundancy
|
|
if (autoName == specifiedName)
|
|
{
|
|
// If the class name does not end with "Prototype", allow the redundancy
|
|
if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding))
|
|
return;
|
|
|
|
var location = argumentSyntax.GetLocation();
|
|
context.ReportDiagnostic(Diagnostic.Create(PrototypeRedundantTypeRule,
|
|
location,
|
|
className,
|
|
specifiedName));
|
|
}
|
|
}
|
|
|
|
private static AttributeSyntax? GetAttributeSyntax(
|
|
AttributeData attributeData,
|
|
ClassDeclarationSyntax classSyntax)
|
|
{
|
|
if (attributeData.ApplicationSyntaxReference is not { } syntaxReference)
|
|
return null;
|
|
|
|
foreach (var attributeList in classSyntax.AttributeLists)
|
|
{
|
|
foreach (var attribute in attributeList.Attributes)
|
|
{
|
|
if (syntaxReference.SyntaxTree != attribute.SyntaxTree)
|
|
continue;
|
|
|
|
if (!syntaxReference.Span.OverlapsWith(attribute.Span))
|
|
continue;
|
|
|
|
return attribute;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|