a43e8285a7
- Avoid generic types in `ScriptPathAttributeGenerator`, this means they won't be added to the `[AssemblyHasScripts]` attribute and a `[ScriptPath]` attribute won't be added to the class. Since generic classes can't be used as scripts they shouldn't use those attributes, this also makes CSharpScript consider those types invalid since they won't be added to the script/type map. - Avoid generic types in `ScriptManagerBridge.LookupScriptsInAssembly`. - Set `outMethodsDest` in `ScriptManagerBridge.UpdateScriptClassInfo`.
184 lines
6.8 KiB
C#
184 lines
6.8 KiB
C#
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.AreGodotSourceGeneratorsDisabled())
|
|
return;
|
|
|
|
if (context.IsGodotToolsProject())
|
|
return;
|
|
|
|
// NOTE: NotNullWhen diagnostics don't work on projects targeting .NET Standard 2.0
|
|
// ReSharper disable once ReplaceWithStringIsNullOrEmpty
|
|
if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir)
|
|
|| godotProjectDir!.Length == 0)
|
|
{
|
|
throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
|
|
}
|
|
|
|
Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses = context
|
|
.Compilation.SyntaxTrees
|
|
.SelectMany(tree =>
|
|
tree.GetRoot().DescendantNodes()
|
|
.OfType<ClassDeclarationSyntax>()
|
|
// Ignore inner classes
|
|
.Where(cds => !cds.IsNested())
|
|
.SelectGodotScriptClasses(context.Compilation)
|
|
// Report and skip non-partial classes
|
|
.Where(x =>
|
|
{
|
|
if (x.cds.IsPartial())
|
|
return true;
|
|
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
|
|
return false;
|
|
})
|
|
)
|
|
.Where(x =>
|
|
// Ignore classes whose name is not the same as the file name
|
|
Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name &&
|
|
// Ignore generic classes
|
|
!x.symbol.IsGenericType)
|
|
.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();
|
|
|
|
// 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");
|
|
|
|
attributes.Append(@"[ScriptPathAttribute(""res://");
|
|
attributes.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir));
|
|
attributes.Append(@""")]");
|
|
}
|
|
|
|
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
|
|
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
|
|
namespaceSymbol.FullQualifiedNameOmitGlobal() :
|
|
string.Empty;
|
|
bool hasNamespace = classNs.Length != 0;
|
|
|
|
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
|
|
+ "_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("\npartial class ");
|
|
source.Append(symbol.NameWithTypeParameters());
|
|
source.Append("\n{\n}\n");
|
|
|
|
if (hasNamespace)
|
|
{
|
|
source.Append("\n}\n");
|
|
}
|
|
|
|
context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
|
|
}
|
|
|
|
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
|
|
.WithGenericsOptions(SymbolDisplayGenericsOptions.None));
|
|
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());
|
|
}
|
|
}
|
|
}
|