Merge pull request #87253 from van800/van800/analyser

Provide a roslyn analyzers corresponding to the GD0001 and GD0002
This commit is contained in:
Rémi Verschelde 2024-02-21 15:22:54 +01:00
commit c6d091e0f3
No known key found for this signature in database
GPG key ID: C3336907360768E1
15 changed files with 230 additions and 76 deletions

View file

@ -0,0 +1,49 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
namespace Godot.SourceGenerators.Tests;
public static class CSharpCodeFixVerifier<TCodeFix, TAnalyzer>
where TCodeFix : CodeFixProvider, new()
where TAnalyzer : DiagnosticAnalyzer, new()
{
public class Test : CSharpCodeFixTest<TAnalyzer, TCodeFix, XUnitVerifier>
{
public Test()
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net60;
SolutionTransforms.Add((Solution solution, ProjectId projectId) =>
{
Project project = solution.GetProject(projectId)!
.AddMetadataReference(Constants.GodotSharpAssembly.CreateMetadataReference());
return project.Solution;
});
}
}
public static Task Verify(string sources, string fixedSources)
{
return MakeVerifier(sources, fixedSources).RunAsync();
}
public static Test MakeVerifier(string source, string results)
{
var verifier = new Test();
verifier.TestCode = File.ReadAllText(Path.Combine(Constants.SourceFolderPath, source));
verifier.FixedCode = File.ReadAllText(Path.Combine(Constants.GeneratedSourceFolderPath, results));
verifier.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", $"""
is_global = true
build_property.GodotProjectDir = {Constants.ExecutingAssemblyPath}
"""));
return verifier;
}
}

View file

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Xunit;
namespace Godot.SourceGenerators.Tests;
public class ClassPartialModifierTest
{
[Fact]
public async Task ClassPartialModifierCodeFixTest()
{
await CSharpCodeFixVerifier<ClassPartialModifierCodeFixProvider, ClassPartialModifierAnalyzer>
.Verify("ClassPartialModifier.GD0001.cs", "ClassPartialModifier.GD0001.fixed.cs");
}
[Fact]
public async void OuterClassPartialModifierAnalyzerTest()
{
await CSharpAnalyzerVerifier<ClassPartialModifierAnalyzer>.Verify("OuterClassPartialModifierAnalyzer.GD0002.cs");
}
}

View file

@ -15,6 +15,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />

View file

@ -0,0 +1,6 @@
using Godot;
public partial class ClassPartialModifier : Node
{
}

View file

@ -0,0 +1,6 @@
using Godot;
public class {|GD0001:ClassPartialModifier|} : Node
{
}

View file

@ -0,0 +1,11 @@
using Godot;
public class {|GD0002:OuterOuterClassPartialModifierAnalyzer|}
{
public class {|GD0002:OuterClassPartialModifierAnalyzer|}
{
// MyNode is contained in a non-partial type so the source generators
// can't enhance this type to work with Godot.
public partial class MyNode : Node { }
}
}

View file

@ -0,0 +1,112 @@
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;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Godot.SourceGenerators
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ClassPartialModifierAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Common.ClassPartialModifierRule, Common.OuterClassPartialModifierRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration);
}
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
if (context.Node is not ClassDeclarationSyntax classDeclaration)
return;
if (context.ContainingSymbol is not INamedTypeSymbol typeSymbol)
return;
if (!typeSymbol.InheritsFrom("GodotSharp", GodotClasses.GodotObject))
return;
if (!classDeclaration.IsPartial())
context.ReportDiagnostic(Diagnostic.Create(
Common.ClassPartialModifierRule,
classDeclaration.Identifier.GetLocation(),
typeSymbol.ToDisplayString()));
var outerClassDeclaration = context.Node.Parent as ClassDeclarationSyntax;
while (outerClassDeclaration is not null)
{
var outerClassTypeSymbol = context.SemanticModel.GetDeclaredSymbol(outerClassDeclaration);
if (outerClassTypeSymbol == null)
return;
if (!outerClassDeclaration.IsPartial())
context.ReportDiagnostic(Diagnostic.Create(
Common.OuterClassPartialModifierRule,
outerClassDeclaration.Identifier.GetLocation(),
outerClassTypeSymbol.ToDisplayString()));
outerClassDeclaration = outerClassDeclaration.Parent as ClassDeclarationSyntax;
}
}
}
[ExportCodeFixProvider(LanguageNames.CSharp)]
public sealed class ClassPartialModifierCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(Common.ClassPartialModifierRule.Id);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
// Get the syntax root of the document.
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
// Get the diagnostic to fix.
var diagnostic = context.Diagnostics.First();
// Get the location of code issue.
var diagnosticSpan = diagnostic.Location.SourceSpan;
// Use that location to find the containing class declaration.
var classDeclaration = root?.FindToken(diagnosticSpan.Start)
.Parent?
.AncestorsAndSelf()
.OfType<ClassDeclarationSyntax>()
.First();
if (classDeclaration == null)
return;
context.RegisterCodeFix(
CodeAction.Create(
"Add partial modifier",
cancellationToken => AddPartialModifierAsync(context.Document, classDeclaration, cancellationToken),
classDeclaration.ToFullString()),
context.Diagnostics);
}
private static async Task<Document> AddPartialModifierAsync(Document document,
ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
{
// Create a new partial modifier.
var partialModifier = SyntaxFactory.Token(SyntaxKind.PartialKeyword);
var modifiedClassDeclaration = classDeclaration.AddModifiers(partialModifier);
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
// Replace the old class declaration with the modified one in the syntax root.
var newRoot = root!.ReplaceNode(classDeclaration, modifiedClassDeclaration);
var newDocument = document.WithSyntaxRoot(newRoot);
return newDocument;
}
}
}

View file

@ -7,63 +7,25 @@ namespace Godot.SourceGenerators
{
private static readonly string _helpLinkFormat = $"{VersionDocsUrl}/tutorials/scripting/c_sharp/diagnostics/{{0}}.html";
public static void ReportNonPartialGodotScriptClass(
GeneratorExecutionContext context,
ClassDeclarationSyntax cds, INamedTypeSymbol symbol
)
{
string message =
"Missing partial modifier on declaration of type '" +
$"{symbol.FullQualifiedNameOmitGlobal()}' that derives from '{GodotClasses.GodotObject}'";
internal static readonly DiagnosticDescriptor ClassPartialModifierRule =
new DiagnosticDescriptor(id: "GD0001",
title: $"Missing partial modifier on declaration of type that derives from '{GodotClasses.GodotObject}'",
messageFormat: $"Missing partial modifier on declaration of type '{{0}}' that derives from '{GodotClasses.GodotObject}'",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
$"Classes that derive from '{GodotClasses.GodotObject}' must be declared with the partial modifier.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0001"));
string description = $"{message}. Classes that derive from '{GodotClasses.GodotObject}' " +
"must be declared with the partial modifier.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GD0001",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description,
helpLinkUri: string.Format(_helpLinkFormat, "GD0001")),
cds.GetLocation(),
cds.SyntaxTree.FilePath));
}
public static void ReportNonPartialGodotScriptOuterClass(
GeneratorExecutionContext context,
TypeDeclarationSyntax outerTypeDeclSyntax
)
{
var outerSymbol = context.Compilation
.GetSemanticModel(outerTypeDeclSyntax.SyntaxTree)
.GetDeclaredSymbol(outerTypeDeclSyntax);
string fullQualifiedName = outerSymbol is INamedTypeSymbol namedTypeSymbol ?
namedTypeSymbol.FullQualifiedNameOmitGlobal() :
"type not found";
string message =
$"Missing partial modifier on declaration of type '{fullQualifiedName}', " +
$"which contains nested classes that derive from '{GodotClasses.GodotObject}'";
string description = $"{message}. Classes that derive from '{GodotClasses.GodotObject}' and their " +
"containing types must be declared with the partial modifier.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GD0002",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description,
helpLinkUri: string.Format(_helpLinkFormat, "GD0002")),
outerTypeDeclSyntax.GetLocation(),
outerTypeDeclSyntax.SyntaxTree.FilePath));
}
internal static readonly DiagnosticDescriptor OuterClassPartialModifierRule =
new DiagnosticDescriptor(id: "GD0002",
title: $"Missing partial modifier on declaration of type which contains nested classes that derive from '{GodotClasses.GodotObject}'",
messageFormat: $"Missing partial modifier on declaration of type '{{0}}' which contains nested classes that derive from '{GodotClasses.GodotObject}'",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
$"Classes that derive from '{GodotClasses.GodotObject}' and their containing types must be declared with the partial modifier.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0002"));
public static readonly DiagnosticDescriptor MultipleClassesInGodotScriptRule =
new DiagnosticDescriptor(id: "GD0003",

View file

@ -26,8 +26,7 @@
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.11.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->

View file

@ -30,16 +30,13 @@ namespace Godot.SourceGenerators
{
if (x.cds.IsPartial())
{
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _))
{
Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
return false;
}
return true;
}
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
.Select(x => x.symbol)

View file

@ -48,7 +48,6 @@ namespace Godot.SourceGenerators
{
if (x.cds.IsPartial())
return true;
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
)

View file

@ -30,16 +30,13 @@ namespace Godot.SourceGenerators
{
if (x.cds.IsPartial())
{
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _))
{
Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
return false;
}
return true;
}
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
.Select(x => x.symbol)

View file

@ -31,16 +31,14 @@ namespace Godot.SourceGenerators
{
if (x.cds.IsPartial())
{
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _))
{
Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
return false;
}
return true;
}
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
.Select(x => x.symbol)

View file

@ -30,16 +30,14 @@ namespace Godot.SourceGenerators
{
if (x.cds.IsPartial())
{
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _))
{
Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
return false;
}
return true;
}
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
.Select(x => x.symbol)

View file

@ -37,16 +37,14 @@ namespace Godot.SourceGenerators
{
if (x.cds.IsPartial())
{
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _))
{
Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
return false;
}
return true;
}
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
.Select(x => x.symbol)