godot/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerato...

183 lines
6.7 KiB
C#
Raw Normal View History

Add C# source generator for a new ScriptPath attribute This source generator adds a newly introduced attribute, `ScriptPath` to all classes that: - Are top-level classes (not inner/nested). - Have the `partial` modifier. - Inherit `Godot.Object`. - The class name matches the file name. A build error is thrown if the generator finds a class that meets these conditions but is not declared `partial`, unless the class is annotated with the `DisableGodotGenerators` attribute. We also generate an `AssemblyHasScripts` assembly attribute which Godot uses to get all the script classes in the assembly, eliminating the need for Godot to search them. We can also avoid searching in assemblies that don't have this attribute. This will be good for performance in the future once we support multiple assemblies with Godot script classes. This is an example of what the generated code looks like: ``` using Godot; namespace Foo { [ScriptPathAttribute("res://Player.cs")] // Multiple partial declarations are allowed [ScriptPathAttribute("res://Foo/Player.cs")] partial class Player {} } [assembly:AssemblyHasScripts(new System.Type[] { typeof(Foo.Player) })] ``` The new attributes replace script metadata which we were generating by determining the namespace of script classes with a very simple parser. This fixes several issues with the old approach related to parser errors and conditional compilation. It also makes the task part of the MSBuild project build, rather than a separate step executed by the Godot editor.
2021-03-05 23:12:42 +00:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptPathAttributeGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
if (context.TryGetGlobalAnalyzerProperty("GodotScriptPathAttributeGenerator", out string? toggle)
&& toggle == "disabled")
{
return;
}
// NOTE: IsNullOrEmpty doesn't work well with nullable checks
// ReSharper disable once ReplaceWithStringIsNullOrEmpty
if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir)
|| godotProjectDir!.Length == 0)
{
throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
}
var godotClasses = context.Compilation.SyntaxTrees
.SelectMany(tree =>
tree.GetRoot().DescendantNodes()
.OfType<ClassDeclarationSyntax>()
// Ignore inner classes
.Where(cds => !(cds.Parent is ClassDeclarationSyntax))
.SelectGodotScriptClasses(context.Compilation)
// Report and skip non-partial classes
.Where(x =>
{
if (x.cds.IsPartial() || x.symbol.HasDisableGeneratorsAttribute())
return true;
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
)
// Ignore classes whose name is not the same as the file name
.Where(x => Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name)
.GroupBy(x => x.symbol)
.ToDictionary(g => g.Key, g => g.Select(x => x.cds));
foreach (var godotClass in godotClasses)
{
VisitGodotScriptClass(context, godotProjectDir,
symbol: godotClass.Key,
classDeclarations: godotClass.Value);
}
if (godotClasses.Count <= 0)
return;
AddScriptTypesAssemblyAttr(context, godotClasses);
}
private static void VisitGodotScriptClass(
GeneratorExecutionContext context,
string godotProjectDir,
INamedTypeSymbol symbol,
IEnumerable<ClassDeclarationSyntax> classDeclarations
)
{
var attributes = new StringBuilder();
Add C# source generator for a new ScriptPath attribute This source generator adds a newly introduced attribute, `ScriptPath` to all classes that: - Are top-level classes (not inner/nested). - Have the `partial` modifier. - Inherit `Godot.Object`. - The class name matches the file name. A build error is thrown if the generator finds a class that meets these conditions but is not declared `partial`, unless the class is annotated with the `DisableGodotGenerators` attribute. We also generate an `AssemblyHasScripts` assembly attribute which Godot uses to get all the script classes in the assembly, eliminating the need for Godot to search them. We can also avoid searching in assemblies that don't have this attribute. This will be good for performance in the future once we support multiple assemblies with Godot script classes. This is an example of what the generated code looks like: ``` using Godot; namespace Foo { [ScriptPathAttribute("res://Player.cs")] // Multiple partial declarations are allowed [ScriptPathAttribute("res://Foo/Player.cs")] partial class Player {} } [assembly:AssemblyHasScripts(new System.Type[] { typeof(Foo.Player) })] ``` The new attributes replace script metadata which we were generating by determining the namespace of script classes with a very simple parser. This fixes several issues with the old approach related to parser errors and conditional compilation. It also makes the task part of the MSBuild project build, rather than a separate step executed by the Godot editor.
2021-03-05 23:12:42 +00:00
// Remember syntax trees for which we already added an attribute, to prevent unnecessary duplicates.
var attributedTrees = new List<SyntaxTree>();
foreach (var cds in classDeclarations)
{
if (attributedTrees.Contains(cds.SyntaxTree))
continue;
attributedTrees.Add(cds.SyntaxTree);
if (attributes.Length != 0)
attributes.Append("\n");
Add C# source generator for a new ScriptPath attribute This source generator adds a newly introduced attribute, `ScriptPath` to all classes that: - Are top-level classes (not inner/nested). - Have the `partial` modifier. - Inherit `Godot.Object`. - The class name matches the file name. A build error is thrown if the generator finds a class that meets these conditions but is not declared `partial`, unless the class is annotated with the `DisableGodotGenerators` attribute. We also generate an `AssemblyHasScripts` assembly attribute which Godot uses to get all the script classes in the assembly, eliminating the need for Godot to search them. We can also avoid searching in assemblies that don't have this attribute. This will be good for performance in the future once we support multiple assemblies with Godot script classes. This is an example of what the generated code looks like: ``` using Godot; namespace Foo { [ScriptPathAttribute("res://Player.cs")] // Multiple partial declarations are allowed [ScriptPathAttribute("res://Foo/Player.cs")] partial class Player {} } [assembly:AssemblyHasScripts(new System.Type[] { typeof(Foo.Player) })] ``` The new attributes replace script metadata which we were generating by determining the namespace of script classes with a very simple parser. This fixes several issues with the old approach related to parser errors and conditional compilation. It also makes the task part of the MSBuild project build, rather than a separate step executed by the Godot editor.
2021-03-05 23:12:42 +00:00
attributes.Append(@"[ScriptPathAttribute(""res://");
attributes.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir));
attributes.Append(@""")]");
Add C# source generator for a new ScriptPath attribute This source generator adds a newly introduced attribute, `ScriptPath` to all classes that: - Are top-level classes (not inner/nested). - Have the `partial` modifier. - Inherit `Godot.Object`. - The class name matches the file name. A build error is thrown if the generator finds a class that meets these conditions but is not declared `partial`, unless the class is annotated with the `DisableGodotGenerators` attribute. We also generate an `AssemblyHasScripts` assembly attribute which Godot uses to get all the script classes in the assembly, eliminating the need for Godot to search them. We can also avoid searching in assemblies that don't have this attribute. This will be good for performance in the future once we support multiple assemblies with Godot script classes. This is an example of what the generated code looks like: ``` using Godot; namespace Foo { [ScriptPathAttribute("res://Player.cs")] // Multiple partial declarations are allowed [ScriptPathAttribute("res://Foo/Player.cs")] partial class Player {} } [assembly:AssemblyHasScripts(new System.Type[] { typeof(Foo.Player) })] ``` The new attributes replace script metadata which we were generating by determining the namespace of script classes with a very simple parser. This fixes several issues with the old approach related to parser errors and conditional compilation. It also makes the task part of the MSBuild project build, rather than a separate step executed by the Godot editor.
2021-03-05 23:12:42 +00:00
}
string className = symbol.Name;
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedName() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
string uniqueName = hasNamespace ?
classNs + "." + className + "_ScriptPath_Generated" :
className + "_ScriptPath_Generated";
var source = new StringBuilder();
// using Godot;
// namespace {classNs} {
// {attributesBuilder}
// partial class {className} { }
// }
source.Append("using Godot;\n");
if (hasNamespace)
{
source.Append("namespace ");
source.Append(classNs);
source.Append(" {\n\n");
}
source.Append(attributes);
source.Append("\n partial class ");
source.Append(className);
source.Append("\n{\n}\n");
if (hasNamespace)
{
source.Append("\n}\n");
}
context.AddSource(uniqueName, SourceText.From(source.ToString(), Encoding.UTF8));
Add C# source generator for a new ScriptPath attribute This source generator adds a newly introduced attribute, `ScriptPath` to all classes that: - Are top-level classes (not inner/nested). - Have the `partial` modifier. - Inherit `Godot.Object`. - The class name matches the file name. A build error is thrown if the generator finds a class that meets these conditions but is not declared `partial`, unless the class is annotated with the `DisableGodotGenerators` attribute. We also generate an `AssemblyHasScripts` assembly attribute which Godot uses to get all the script classes in the assembly, eliminating the need for Godot to search them. We can also avoid searching in assemblies that don't have this attribute. This will be good for performance in the future once we support multiple assemblies with Godot script classes. This is an example of what the generated code looks like: ``` using Godot; namespace Foo { [ScriptPathAttribute("res://Player.cs")] // Multiple partial declarations are allowed [ScriptPathAttribute("res://Foo/Player.cs")] partial class Player {} } [assembly:AssemblyHasScripts(new System.Type[] { typeof(Foo.Player) })] ``` The new attributes replace script metadata which we were generating by determining the namespace of script classes with a very simple parser. This fixes several issues with the old approach related to parser errors and conditional compilation. It also makes the task part of the MSBuild project build, rather than a separate step executed by the Godot editor.
2021-03-05 23:12:42 +00:00
}
private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context,
Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses)
{
var sourceBuilder = new StringBuilder();
sourceBuilder.Append("[assembly:");
sourceBuilder.Append(GodotClasses.AssemblyHasScriptsAttr);
sourceBuilder.Append("(new System.Type[] {");
bool first = true;
foreach (var godotClass in godotClasses)
{
var qualifiedName = godotClass.Key.ToDisplayString(
NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat);
if (!first)
sourceBuilder.Append(", ");
first = false;
sourceBuilder.Append("typeof(");
sourceBuilder.Append(qualifiedName);
sourceBuilder.Append(")");
}
sourceBuilder.Append("})]\n");
context.AddSource("AssemblyScriptTypes_Generated",
SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
public void Initialize(GeneratorInitializationContext context)
{
}
private static string RelativeToDir(string path, string dir)
{
// Make sure the directory ends with a path separator
dir = Path.Combine(dir, " ").TrimEnd();
if (Path.DirectorySeparatorChar == '\\')
dir = dir.Replace("/", "\\") + "\\";
var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
// MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
}
}
}