diff options
Diffstat (limited to 'modules/mono/editor/Godot.NET.Sdk')
14 files changed, 473 insertions, 45 deletions
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln index 56c0cb7703..d1868f52ef 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln @@ -2,6 +2,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Sample", "Godot.SourceGenerators.Sample\Godot.SourceGenerators.Sample.csproj", "{7297A614-8DF5-43DE-9EAD-99671B26BD1F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotSharp", "..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj", "{AEBF0036-DA76-4341-B651-A3F2856AB2FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +18,17 @@ Global {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU + {32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Release|Any CPU.Build.0 = Release|Any CPU + {7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.Build.0 = Release|Any CPU + {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj index 86a0a4393e..4e9e7184da 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -1,35 +1,41 @@ <Project Sdk="Microsoft.Build.NoTargets/2.0.1"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <Description>MSBuild .NET Sdk for Godot projects.</Description> <Authors>Godot Engine contributors</Authors> <PackageId>Godot.NET.Sdk</PackageId> <Version>4.0.0</Version> - <PackageVersion>4.0.0-dev2</PackageVersion> - <PackageProjectUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</PackageProjectUrl> + <PackageVersion>$(PackageVersion_Godot_NET_Sdk)</PackageVersion> + <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</RepositoryUrl> + <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> <PackageType>MSBuildSdk</PackageType> <PackageTags>MSBuildSdk</PackageTags> + <PackageLicenseExpression>MIT</PackageLicenseExpression> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> - </PropertyGroup> - <PropertyGroup> - <NuspecFile>Godot.NET.Sdk.nuspec</NuspecFile> - <GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuSpecProperties</GenerateNuspecDependsOn> + <!-- Exclude target framework from the package dependencies as we don't include the build output --> + <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking> + <IncludeBuildOutput>false</IncludeBuildOutput> </PropertyGroup> - <Target Name="SetNuSpecProperties" Condition=" Exists('$(NuspecFile)') "> + <ItemGroup> + <!-- Package Sdk\Sdk.props and Sdk\Sdk.targets file --> + <None Include="Sdk\Sdk.props" Pack="true" PackagePath="Sdk" /> + <None Include="Sdk\Sdk.targets" Pack="true" PackagePath="Sdk" /> + <!-- SdkPackageVersions.props --> + <None Include="..\..\..\SdkPackageVersions.props" Pack="true" PackagePath="Sdk"> + <Link>Sdk\SdkPackageVersions.props</Link> + </None> + </ItemGroup> + + <Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack"> <PropertyGroup> - <NuspecProperties> - id=$(PackageId); - description=$(Description); - authors=$(Authors); - version=$(PackageVersion); - packagetype=$(PackageType); - tags=$(PackageTags); - projecturl=$(PackageProjectUrl) - </NuspecProperties> + <GodotSourceRootPath>$(SolutionDir)\..\..\..\..\</GodotSourceRootPath> + <GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir> </PropertyGroup> + <Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" /> </Target> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec deleted file mode 100644 index 5b5cefe80e..0000000000 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8" ?> -<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd"> - <metadata> - <id>$id$</id> - <version>$version$</version> - <description>$description$</description> - <authors>$authors$</authors> - <owners>$authors$</owners> - <projectUrl>$projecturl$</projectUrl> - <requireLicenseAcceptance>false</requireLicenseAcceptance> - <license type="expression">MIT</license> - <licenseUrl>https://licenses.nuget.org/MIT</licenseUrl> - <tags>$tags$</tags> - <packageTypes> - <packageType name="$packagetype$" /> - </packageTypes> - <repository url="$projecturl$" /> - </metadata> - <files> - <file src="Sdk\**" target="Sdk" />\ - </files> -</package> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props index 4f8faffde2..0128f5c706 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -1,4 +1,6 @@ <Project> + <Import Project="$(MSBuildThisFileDirectory)\SdkPackageVersions.props" /> + <PropertyGroup> <!-- Determines if we should import Microsoft.NET.Sdk, if it wasn't already imported. --> <GodotSdkImportsMicrosoftNetSdk Condition=" '$(UsingMicrosoftNETSdk)' != 'true' ">true</GodotSdkImportsMicrosoftNetSdk> @@ -14,15 +16,15 @@ <GodotProjectDir Condition=" '$(SolutionDir)' == '' ">$(MSBuildProjectDirectory)</GodotProjectDir> <GodotProjectDir>$([MSBuild]::EnsureTrailingSlash('$(GodotProjectDir)'))</GodotProjectDir> - <!-- Custom output paths for Godot projects. In brief, 'bin\' and 'obj\' are moved to '$(GodotProjectDir)\.mono\temp\'. --> - <BaseOutputPath>$(GodotProjectDir).mono\temp\bin\</BaseOutputPath> - <OutputPath>$(GodotProjectDir).mono\temp\bin\$(Configuration)\</OutputPath> + <!-- Custom output paths for Godot projects. In brief, 'bin\' and 'obj\' are moved to '$(GodotProjectDir)\.godot\mono\temp\'. --> + <BaseOutputPath>$(GodotProjectDir).godot\mono\temp\bin\</BaseOutputPath> + <OutputPath>$(GodotProjectDir).godot\mono\temp\bin\$(Configuration)\</OutputPath> <!-- Use custom IntermediateOutputPath and BaseIntermediateOutputPath only if it wasn't already set. Otherwise the old values may have already been changed by MSBuild which can cause problems with NuGet. --> - <IntermediateOutputPath Condition=" '$(IntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\$(Configuration)\</IntermediateOutputPath> - <BaseIntermediateOutputPath Condition=" '$(BaseIntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\</BaseIntermediateOutputPath> + <IntermediateOutputPath Condition=" '$(IntermediateOutputPath)' == '' ">$(GodotProjectDir).godot\mono\temp\obj\$(Configuration)\</IntermediateOutputPath> + <BaseIntermediateOutputPath Condition=" '$(BaseIntermediateOutputPath)' == '' ">$(GodotProjectDir).godot\mono\temp\obj\</BaseIntermediateOutputPath> <!-- Do not append the target framework name to the output path. --> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> @@ -94,6 +96,7 @@ <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants> </PropertyGroup> + <!-- Godot API references --> <ItemGroup> <!-- TODO: @@ -102,11 +105,11 @@ --> <Reference Include="GodotSharp"> <Private>false</Private> - <HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath> + <HintPath>$(GodotProjectDir).godot\mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath> </Reference> <Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' "> <Private>false</Private> - <HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath> + <HintPath>$(GodotProjectDir).godot\mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath> </Reference> </ItemGroup> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets index f5afd75505..92e299d2f3 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets @@ -14,4 +14,9 @@ --> <DefineConstants Condition=" '$(GodotRealTIsDouble)' == 'true' ">GODOT_REAL_T_IS_DOUBLE;$(DefineConstants)</DefineConstants> </PropertyGroup> + + <!-- C# source generators --> + <ItemGroup Condition=" '$(DisableImplicitGodotGeneratorReferences)' != 'true' "> + <PackageReference Include="Godot.SourceGenerators" Version="$(PackageVersion_Godot_SourceGenerators)" /> + </ItemGroup> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Bar.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Bar.cs new file mode 100644 index 0000000000..5eaebc4474 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Bar.cs @@ -0,0 +1,15 @@ +namespace Godot.SourceGenerators.Sample +{ + partial class Bar : Godot.Object + { + } + + // Foo in another file + partial class Foo + { + } + + partial class NotSameNameAsFile : Godot.Object + { + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Foo.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Foo.cs new file mode 100644 index 0000000000..21a5bfe560 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Foo.cs @@ -0,0 +1,11 @@ +namespace Godot.SourceGenerators.Sample +{ + partial class Foo : Godot.Object + { + } + + // Foo again in the same file + partial class Foo + { + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj new file mode 100644 index 0000000000..24f7909861 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj @@ -0,0 +1,31 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.1</TargetFramework> + </PropertyGroup> + + <PropertyGroup> + <!-- $(GodotProjectDir) would normally be defined by the Godot.NET.Sdk --> + <GodotProjectDir>$(MSBuildProjectDirectory)</GodotProjectDir> + </PropertyGroup> + + <PropertyGroup> + <!-- The emitted files are not part of the compilation nor design. + They're only for peeking at the generated sources. Sometimes the + emitted files get corrupted, but that won't break anything. --> + <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> + <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj"> + <Private>False</Private> + </ProjectReference> + <ProjectReference Include="..\Godot.SourceGenerators\Godot.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + </ItemGroup> + + <!-- This file is imported automatically when using PackageReference to + reference Godot.SourceGenerators, but not when using ProjectReference --> + <Import Project="..\Godot.SourceGenerators\Godot.SourceGenerators.props" /> + +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs new file mode 100644 index 0000000000..4867c986e6 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Godot.SourceGenerators +{ + public static class Common + { + public static void ReportNonPartialGodotScriptClass( + GeneratorExecutionContext context, + ClassDeclarationSyntax cds, INamedTypeSymbol symbol + ) + { + string message = + "Missing partial modifier on declaration of type '" + + $"{symbol.FullQualifiedName()}' which is a subclass of '{GodotClasses.Object}'"; + + string description = $"{message}. Subclasses of '{GodotClasses.Object}' must be " + + "declared with the partial modifier or annotated with the " + + $"attribute '{GodotClasses.DisableGodotGeneratorsAttr}'."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0001", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + cds.GetLocation(), + cds.SyntaxTree.FilePath)); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs new file mode 100644 index 0000000000..e16f72f43a --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Godot.SourceGenerators +{ + static class ExtensionMethods + { + public static bool TryGetGlobalAnalyzerProperty( + this GeneratorExecutionContext context, string property, out string? value + ) => context.AnalyzerConfigOptions.GlobalOptions + .TryGetValue("build_property." + property, out value); + + private static bool InheritsFrom(this INamedTypeSymbol? symbol, string baseName) + { + if (symbol == null) + return false; + + while (true) + { + if (symbol.ToString() == baseName) + { + return true; + } + + if (symbol.BaseType != null) + { + symbol = symbol.BaseType; + continue; + } + + break; + } + + return false; + } + + private static bool IsGodotScriptClass( + 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(GodotClasses.Object)) + { + symbol = null; + return false; + } + + symbol = classTypeSymbol; + return true; + } + + public static IEnumerable<(ClassDeclarationSyntax cds, INamedTypeSymbol symbol)> SelectGodotScriptClasses( + this IEnumerable<ClassDeclarationSyntax> source, + Compilation compilation + ) + { + foreach (var cds in source) + { + if (cds.IsGodotScriptClass(compilation, out var symbol)) + yield return (cds, symbol!); + } + } + + public static bool IsPartial(this ClassDeclarationSyntax cds) + => cds.Modifiers.Any(SyntaxKind.PartialKeyword); + + public static bool HasDisableGeneratorsAttribute(this INamedTypeSymbol symbol) + => symbol.GetAttributes().Any(attr => + attr.AttributeClass?.ToString() == GodotClasses.DisableGodotGeneratorsAttr); + + private static SymbolDisplayFormat FullyQualifiedFormatOmitGlobal { get; } = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted); + + public static string FullQualifiedName(this INamedTypeSymbol symbol) + => symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatOmitGlobal); + + public static string FullQualifiedName(this INamespaceSymbol namespaceSymbol) + => namespaceSymbol.ToDisplayString(FullyQualifiedFormatOmitGlobal); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj new file mode 100644 index 0000000000..11d8e0f72b --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj @@ -0,0 +1,41 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>8.0</LangVersion> + <Nullable>enable</Nullable> + </PropertyGroup> + <PropertyGroup> + <Description>Core C# source generator for Godot projects.</Description> + <Authors>Godot Engine contributors</Authors> + + <PackageId>Godot.SourceGenerators</PackageId> + <Version>4.0.0</Version> + <PackageVersion>$(PackageVersion_Godot_SourceGenerators)</PackageVersion> + <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators</RepositoryUrl> + <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> + <PackageLicenseExpression>MIT</PackageLicenseExpression> + + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <!-- Do not include the generator as a lib dependency --> + <IncludeBuildOutput>false</IncludeBuildOutput> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" /> + </ItemGroup> + <ItemGroup> + <!-- Package the generator in the analyzer directory of the nuget package --> + <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> + + <!-- Package the props file --> + <None Include="Godot.SourceGenerators.props" Pack="true" PackagePath="build" Visible="false" /> + </ItemGroup> + + <Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack"> + <PropertyGroup> + <GodotSourceRootPath>$(SolutionDir)\..\..\..\..\</GodotSourceRootPath> + <GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir> + </PropertyGroup> + <Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" /> + </Target> +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props new file mode 100644 index 0000000000..f9b47ad5b1 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props @@ -0,0 +1,7 @@ +<Project> + <ItemGroup> + <!-- $(GodotProjectDir) is defined by Godot.NET.Sdk --> + <CompilerVisibleProperty Include="GodotProjectDir" /> + <CompilerVisibleProperty Include="GodotScriptPathAttributeGenerator" /> + </ItemGroup> +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs new file mode 100644 index 0000000000..29e41d155a --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs @@ -0,0 +1,9 @@ +namespace Godot.SourceGenerators +{ + public static class GodotClasses + { + public const string Object = "Godot.Object"; + public const string DisableGodotGeneratorsAttr = "Godot.DisableGodotGeneratorsAttribute"; + public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute"; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs new file mode 100644 index 0000000000..a51728e221 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -0,0 +1,182 @@ +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(); + + // 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(@""")]"); + } + + 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)); + } + + 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()); + } + } +} |