123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- 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;
- })
- )
- // 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();
- // 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.FullQualifiedName() :
- string.Empty;
- bool hasNamespace = classNs.Length != 0;
- string uniqueHint = symbol.FullQualifiedName().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());
- }
- }
- }
|