using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Godot.SourceGenerators { internal static class ExtensionMethods { public static bool TryGetGlobalAnalyzerProperty( this GeneratorExecutionContext context, string property, out string? value ) => context.AnalyzerConfigOptions.GlobalOptions .TryGetValue("build_property." + property, out value); public static bool AreGodotSourceGeneratorsDisabled(this GeneratorExecutionContext context) => context.TryGetGlobalAnalyzerProperty("GodotSourceGenerators", out string? toggle) && toggle != null && toggle.Equals("disabled", StringComparison.OrdinalIgnoreCase); public static bool IsGodotToolsProject(this GeneratorExecutionContext context) => context.TryGetGlobalAnalyzerProperty("IsGodotToolsProject", out string? toggle) && toggle != null && toggle.Equals("true", StringComparison.OrdinalIgnoreCase); public static bool IsGodotSourceGeneratorDisabled(this GeneratorExecutionContext context, string generatorName) => AreGodotSourceGeneratorsDisabled(context) || (context.TryGetGlobalAnalyzerProperty("GodotDisabledSourceGenerators", out string? disabledGenerators) && disabledGenerators != null && disabledGenerators.Split(';').Contains(generatorName)); public static bool InheritsFrom(this ITypeSymbol? symbol, string assemblyName, string typeFullName) { while (symbol != null) { if (symbol.ContainingAssembly?.Name == assemblyName && symbol.FullQualifiedNameOmitGlobal() == typeFullName) { return true; } symbol = symbol.BaseType; } return false; } public static INamedTypeSymbol? GetGodotScriptNativeClass(this INamedTypeSymbol classTypeSymbol) { var symbol = classTypeSymbol; while (symbol != null) { if (symbol.ContainingAssembly?.Name == "GodotSharp") return symbol; symbol = symbol.BaseType; } return null; } public static string? GetGodotScriptNativeClassName(this INamedTypeSymbol classTypeSymbol) { var nativeType = classTypeSymbol.GetGodotScriptNativeClass(); if (nativeType == null) return null; var godotClassNameAttr = nativeType.GetAttributes() .FirstOrDefault(a => a.AttributeClass?.IsGodotClassNameAttribute() ?? false); string? godotClassName = null; if (godotClassNameAttr is { ConstructorArguments: { Length: > 0 } }) godotClassName = godotClassNameAttr.ConstructorArguments[0].Value?.ToString(); return godotClassName ?? nativeType.Name; } private static bool TryGetGodotScriptClass( this ClassDeclarationSyntax cds, Compilation compilation, out INamedTypeSymbol? symbol ) { var sm = compilation.GetSemanticModel(cds.SyntaxTree); var classTypeSymbol = sm.GetDeclaredSymbol(cds); if (classTypeSymbol?.BaseType == null || !classTypeSymbol.BaseType.InheritsFrom("GodotSharp", GodotClasses.GodotObject)) { symbol = null; return false; } symbol = classTypeSymbol; return true; } public static IEnumerable<(ClassDeclarationSyntax cds, INamedTypeSymbol symbol)> SelectGodotScriptClasses( this IEnumerable source, Compilation compilation ) { foreach (var cds in source) { if (cds.TryGetGodotScriptClass(compilation, out var symbol)) yield return (cds, symbol!); } } public static bool IsNested(this TypeDeclarationSyntax cds) => cds.Parent is TypeDeclarationSyntax; public static bool IsPartial(this TypeDeclarationSyntax cds) => cds.Modifiers.Any(SyntaxKind.PartialKeyword); public static bool AreAllOuterTypesPartial( this TypeDeclarationSyntax cds, out TypeDeclarationSyntax? typeMissingPartial ) { SyntaxNode? outerSyntaxNode = cds.Parent; while (outerSyntaxNode is TypeDeclarationSyntax outerTypeDeclSyntax) { if (!outerTypeDeclSyntax.IsPartial()) { typeMissingPartial = outerTypeDeclSyntax; return false; } outerSyntaxNode = outerSyntaxNode.Parent; } typeMissingPartial = null; return true; } public static string GetDeclarationKeyword(this INamedTypeSymbol namedTypeSymbol) { string? keyword = namedTypeSymbol.DeclaringSyntaxReferences .OfType().FirstOrDefault()? .Keyword.Text; return keyword ?? namedTypeSymbol.TypeKind switch { TypeKind.Interface => "interface", TypeKind.Struct => "struct", _ => "class" }; } public static string NameWithTypeParameters(this INamedTypeSymbol symbol) { return symbol.IsGenericType ? string.Concat(symbol.Name, "<", string.Join(", ", symbol.TypeParameters), ">") : symbol.Name; } private static SymbolDisplayFormat FullyQualifiedFormatOmitGlobal { get; } = SymbolDisplayFormat.FullyQualifiedFormat .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted); private static SymbolDisplayFormat FullyQualifiedFormatIncludeGlobal { get; } = SymbolDisplayFormat.FullyQualifiedFormat .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Included); public static string FullQualifiedNameOmitGlobal(this ITypeSymbol symbol) => symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatOmitGlobal); public static string FullQualifiedNameOmitGlobal(this INamespaceSymbol namespaceSymbol) => namespaceSymbol.ToDisplayString(FullyQualifiedFormatOmitGlobal); public static string FullQualifiedNameIncludeGlobal(this ITypeSymbol symbol) => symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatIncludeGlobal); public static string FullQualifiedNameIncludeGlobal(this INamespaceSymbol namespaceSymbol) => namespaceSymbol.ToDisplayString(FullyQualifiedFormatIncludeGlobal); public static string FullQualifiedSyntax(this SyntaxNode node, SemanticModel sm) { StringBuilder sb = new(); FullQualifiedSyntax(node, sm, sb, true); return sb.ToString(); } private static void FullQualifiedSyntax(SyntaxNode node, SemanticModel sm, StringBuilder sb, bool isFirstNode) { if (node is NameSyntax ns && isFirstNode) { SymbolInfo nameInfo = sm.GetSymbolInfo(ns); sb.Append(nameInfo.Symbol?.ToDisplayString(FullyQualifiedFormatIncludeGlobal) ?? ns.ToString()); return; } bool innerIsFirstNode = true; foreach (var child in node.ChildNodesAndTokens()) { if (child.HasLeadingTrivia) { sb.Append(child.GetLeadingTrivia()); } if (child.IsNode) { FullQualifiedSyntax(child.AsNode()!, sm, sb, isFirstNode: innerIsFirstNode); innerIsFirstNode = false; } else { sb.Append(child); } if (child.HasTrailingTrivia) { sb.Append(child.GetTrailingTrivia()); } } } public static string SanitizeQualifiedNameForUniqueHint(this string qualifiedName) => qualifiedName // AddSource() doesn't support angle brackets .Replace("<", "(Of ") .Replace(">", ")"); public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol) => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.ExportAttr; public static bool IsGodotSignalAttribute(this INamedTypeSymbol symbol) => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.SignalAttr; public static bool IsGodotMustBeVariantAttribute(this INamedTypeSymbol symbol) => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.MustBeVariantAttr; public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol) => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.GodotClassNameAttr; public static bool IsGodotGlobalClassAttribute(this INamedTypeSymbol symbol) => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.GlobalClassAttr; public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol) => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.SystemFlagsAttr; public static GodotMethodData? HasGodotCompatibleSignature( this IMethodSymbol method, MarshalUtils.TypeCache typeCache ) { if (method.IsGenericMethod) return null; var retSymbol = method.ReturnType; var retType = method.ReturnsVoid ? null : MarshalUtils.ConvertManagedTypeToMarshalType(method.ReturnType, typeCache); if (retType == null && !method.ReturnsVoid) return null; var parameters = method.Parameters; var paramTypes = parameters // Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may) .Where(p => p.RefKind == RefKind.None) // Attempt to determine the variant type .Select(p => MarshalUtils.ConvertManagedTypeToMarshalType(p.Type, typeCache)) // Discard parameter types that couldn't be determined (null entries) .Where(t => t != null).Cast().ToImmutableArray(); // If any parameter type was incompatible, it was discarded so the length won't match if (parameters.Length > paramTypes.Length) return null; // Ignore incompatible method return new GodotMethodData(method, paramTypes, parameters.Select(p => p.Type).ToImmutableArray(), retType != null ? (retType.Value, retSymbol) : null); } public static IEnumerable WhereHasGodotCompatibleSignature( this IEnumerable methods, MarshalUtils.TypeCache typeCache ) { foreach (var method in methods) { var methodData = HasGodotCompatibleSignature(method, typeCache); if (methodData != null) yield return methodData.Value; } } public static IEnumerable WhereIsGodotCompatibleType( this IEnumerable properties, MarshalUtils.TypeCache typeCache ) { foreach (var property in properties) { var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(property.Type, typeCache); if (marshalType == null) continue; yield return new GodotPropertyData(property, marshalType.Value); } } public static IEnumerable WhereIsGodotCompatibleType( this IEnumerable fields, MarshalUtils.TypeCache typeCache ) { foreach (var field in fields) { // TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(field.Type, typeCache); if (marshalType == null) continue; yield return new GodotFieldData(field, marshalType.Value); } } public static Location? FirstLocationWithSourceTreeOrDefault(this IEnumerable locations) { return locations.FirstOrDefault(location => location.SourceTree != null) ?? locations.FirstOrDefault(); } public static string Path(this Location location) => location.SourceTree?.GetLineSpan(location.SourceSpan).Path ?? location.GetLineSpan().Path; public static int StartLine(this Location location) => location.SourceTree?.GetLineSpan(location.SourceSpan).StartLinePosition.Line ?? location.GetLineSpan().StartLinePosition.Line; } }