C#: Various fixes to generic scripts

- Report a diagnostic when there are multiple classes that match the script file name in the same script since that will result in a duplicate path key in the bimap and it's not allowed.
- Fix InspectorPlugin to handle empty paths in case the project was built with a previous version of Godot that used empty paths for generic scripts.
- Add tests for the new diagnostic GD0003.
This commit is contained in:
Raul Santos 2024-02-15 18:18:33 +01:00
parent ae51db75e7
commit fe280ef9ae
No known key found for this signature in database
GPG key ID: B532473AE3A803E4
12 changed files with 126 additions and 28 deletions

View file

@ -2,17 +2,6 @@
namespace Godot.SourceGenerators.Sample
{
partial class Generic<T> : GodotObject
{
private int _field;
}
// Generic again but different generic parameters
partial class Generic<T, R> : GodotObject
{
private int _field;
}
// Generic again but without generic parameters
partial class Generic : GodotObject
{

View file

@ -0,0 +1,9 @@
#pragma warning disable CS0169
namespace Godot.SourceGenerators.Sample
{
partial class Generic1T<T> : GodotObject
{
private int _field;
}
}

View file

@ -0,0 +1,10 @@
#pragma warning disable CS0169
namespace Godot.SourceGenerators.Sample
{
// Generic again but different generic parameters
partial class Generic2T<T, R> : GodotObject
{
private int _field;
}
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -47,9 +48,33 @@ public class ScriptPathAttributeGeneratorTests
{
var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier(
new string[] { "Generic.cs" },
new string[] { "Generic_ScriptPath.generated.cs" }
new string[] { "Generic(Of T)_ScriptPath.generated.cs" }
);
verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Generic" }));
verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Generic<>" }));
await verifier.RunAsync();
}
[Fact]
public async void GenericMultipleClassesSameName()
{
var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier(
Array.Empty<string>(),
new string[] { "Generic(Of T)_ScriptPath.generated.cs" }
);
verifier.TestState.Sources.Add(("Generic.cs", File.ReadAllText(Path.Combine(Constants.SourceFolderPath, "Generic.GD0003.cs"))));
verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Generic<>", "global::Generic<,>", "global::Generic" }));
await verifier.RunAsync();
}
[Fact]
public async void NamespaceMultipleClassesSameName()
{
var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier(
Array.Empty<string>(),
new string[] { "NamespaceA.SameName_ScriptPath.generated.cs" }
);
verifier.TestState.Sources.Add(("SameName.cs", File.ReadAllText(Path.Combine(Constants.SourceFolderPath, "SameName.GD0003.cs"))));
verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::NamespaceA.SameName", "global::NamespaceB.SameName" }));
await verifier.RunAsync();
}
}

View file

@ -0,0 +1,9 @@
using Godot;
namespace NamespaceA {
[ScriptPathAttribute("res://SameName.cs")]
partial class SameName
{
}
}

View file

@ -0,0 +1,18 @@
using Godot;
partial class Generic<T> : GodotObject
{
private int _field;
}
// Generic again but different generic parameters
partial class {|GD0003:Generic|}<T, R> : GodotObject
{
private int _field;
}
// Generic again but without generic parameters
partial class {|GD0003:Generic|} : GodotObject
{
private int _field;
}

View file

@ -4,15 +4,3 @@ partial class Generic<T> : GodotObject
{
private int _field;
}
// Generic again but different generic parameters
partial class Generic<T, R> : GodotObject
{
private int _field;
}
// Generic again but without generic parameters
partial class Generic : GodotObject
{
private int _field;
}

View file

@ -0,0 +1,18 @@
using Godot;
namespace NamespaceA
{
partial class SameName : GodotObject
{
private int _field;
}
}
// SameName again but different namespace
namespace NamespaceB
{
partial class {|GD0003:SameName|} : GodotObject
{
private int _field;
}
}

View file

@ -65,6 +65,16 @@ namespace Godot.SourceGenerators
outerTypeDeclSyntax.SyntaxTree.FilePath));
}
public static readonly DiagnosticDescriptor MultipleClassesInGodotScriptRule =
new DiagnosticDescriptor(id: "GD0003",
title: "Found multiple classes with the same name in the same script file",
messageFormat: "Found multiple classes with the name '{0}' in the same script file",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"Found multiple classes with the same name in the same script file. A script file must only contain one class with a name that matches the file name.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0003"));
public static readonly DiagnosticDescriptor ExportedMemberIsStaticRule =
new DiagnosticDescriptor(id: "GD0101",
title: "The exported member is static",

View file

@ -58,9 +58,10 @@ namespace Godot.SourceGenerators
.GroupBy<(ClassDeclarationSyntax cds, INamedTypeSymbol symbol), INamedTypeSymbol>(x => x.symbol, SymbolEqualityComparer.Default)
.ToDictionary<IGrouping<INamedTypeSymbol, (ClassDeclarationSyntax cds, INamedTypeSymbol symbol)>, INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>>(g => g.Key, g => g.Select(x => x.cds), SymbolEqualityComparer.Default);
var usedPaths = new HashSet<string>();
foreach (var godotClass in godotClasses)
{
VisitGodotScriptClass(context, godotProjectDir,
VisitGodotScriptClass(context, godotProjectDir, usedPaths,
symbol: godotClass.Key,
classDeclarations: godotClass.Value);
}
@ -74,6 +75,7 @@ namespace Godot.SourceGenerators
private static void VisitGodotScriptClass(
GeneratorExecutionContext context,
string godotProjectDir,
HashSet<string> usedPaths,
INamedTypeSymbol symbol,
IEnumerable<ClassDeclarationSyntax> classDeclarations
)
@ -93,8 +95,19 @@ namespace Godot.SourceGenerators
if (attributes.Length != 0)
attributes.Append("\n");
string scriptPath = RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir);
if (!usedPaths.Add(scriptPath))
{
context.ReportDiagnostic(Diagnostic.Create(
Common.MultipleClassesInGodotScriptRule,
cds.Identifier.GetLocation(),
symbol.Name
));
return;
}
attributes.Append(@"[ScriptPathAttribute(""res://");
attributes.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir));
attributes.Append(scriptPath);
attributes.Append(@""")]");
}

View file

@ -28,6 +28,15 @@ namespace GodotTools.Inspector
continue;
string scriptPath = script.ResourcePath;
if (string.IsNullOrEmpty(scriptPath))
{
// Generic types used empty paths in older versions of Godot
// so we assume your project is out of sync.
AddCustomControl(new InspectorOutOfSyncWarning());
break;
}
if (scriptPath.StartsWith("csharp://"))
{
// This is a virtual path used by generic types, extract the real path.