00dc19585b
Co-authored-by: Raul Santos <raulsntos@gmail.com> Co-authored-by: A Thousand Ships <96648715+AThousandShips@users.noreply.github.com>
203 lines
7.9 KiB
C#
203 lines
7.9 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.IsGodotSourceGeneratorDisabled("ScriptPathAttribute"))
|
|
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("GodotProjectDirBase64", out string? godotProjectDir) || godotProjectDir!.Length == 0)
|
|
{
|
|
if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out godotProjectDir) || godotProjectDir!.Length == 0)
|
|
{
|
|
throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Workaround for https://github.com/dotnet/roslyn/issues/51692
|
|
godotProjectDir = Encoding.UTF8.GetString(Convert.FromBase64String(godotProjectDir));
|
|
}
|
|
|
|
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;
|
|
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)
|
|
.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, usedPaths,
|
|
symbol: godotClass.Key,
|
|
classDeclarations: godotClass.Value);
|
|
}
|
|
|
|
if (godotClasses.Count <= 0)
|
|
return;
|
|
|
|
AddScriptTypesAssemblyAttr(context, godotClasses);
|
|
}
|
|
|
|
private static void VisitGodotScriptClass(
|
|
GeneratorExecutionContext context,
|
|
string godotProjectDir,
|
|
HashSet<string> usedPaths,
|
|
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");
|
|
|
|
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(scriptPath);
|
|
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);
|
|
if (godotClass.Key.IsGenericType)
|
|
sourceBuilder.Append($"<{new string(',', godotClass.Key.TypeParameters.Count() - 1)}>");
|
|
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());
|
|
}
|
|
}
|
|
}
|