using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System.Collections.Generic; using System.IO; using System.Reflection; namespace Generator { [TestClass] public class CodeGen { private void AddTree( List syntaxTree, string path ) { var parseOptions = CSharpParseOptions.Default.WithLanguageVersion( LanguageVersion.Default ); var code = System.IO.File.ReadAllText( path ); var tree = CSharpSyntaxTree.ParseText( text: code, options: parseOptions, path: path, encoding: System.Text.Encoding.UTF8 ); syntaxTree.Add( tree ); } CSharpCompilation Build( string assemblyName, params string[] files ) { List SyntaxTree = new List(); foreach ( var file in files ) AddTree( SyntaxTree, $"data/codegen/{file}" ); var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) .WithConcurrentBuild( true ) .WithOptimizationLevel( OptimizationLevel.Debug ) .WithGeneralDiagnosticOption( ReportDiagnostic.Info ) .WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu ) .WithAllowUnsafe( false ); var refs = new List(); var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location ); refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) ); refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); // Sandbox.System CSharpCompilation compiler = CSharpCompilation.Create( $"{assemblyName}.dll", SyntaxTree, refs, optn ); var processor = new Sandbox.Generator.Processor(); processor.AddonName = assemblyName; processor.PackageAssetResolver = ( p ) => $"/{p}/model_mock.mdl"; processor.Run( compiler ); compiler = processor.Compilation; foreach ( var tree in compiler.SyntaxTrees ) { System.Console.WriteLine( tree.FilePath ); System.Console.WriteLine( tree.GetText().ToString() ); } return compiler; } CSharpCompilation BuildString( string contents, string assemblyName = "assembly_name" ) { List SyntaxTree = new List(); { var parseOptions = CSharpParseOptions.Default.WithLanguageVersion( LanguageVersion.Default ); var code = contents; var tree = CSharpSyntaxTree.ParseText( text: code, options: parseOptions, path: "Program.cs", encoding: System.Text.Encoding.UTF8 ); SyntaxTree.Add( tree ); } var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) .WithConcurrentBuild( true ) .WithOptimizationLevel( OptimizationLevel.Debug ) .WithGeneralDiagnosticOption( ReportDiagnostic.Info ) .WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu ) .WithAllowUnsafe( false ); var refs = new List(); var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location ); refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) ); refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); // Sandbox.System CSharpCompilation compiler = CSharpCompilation.Create( $"{assemblyName}.dll", SyntaxTree, refs, optn ); var processor = new Sandbox.Generator.Processor(); processor.AddonName = assemblyName; processor.PackageAssetResolver = ( p ) => $"/{p}/model_mock.mdl"; processor.Run( compiler ); compiler = processor.Compilation; foreach ( var tree in compiler.SyntaxTrees ) { System.Console.WriteLine( tree.FilePath ); System.Console.WriteLine( tree.GetText().ToString() ); } return compiler; } [TestMethod] [DataRow( "TestVar.cs" )] [DataRow( "TestRpc.cs" )] [DataRow( "TestReplicate.cs" )] [DataRow( "TestStackOverflow.cs" )] [DataRow( "TestRpc.Entity.cs" )] [DataRow( "TestReplicate.Entity.cs" )] public void DoCodeGen( string filename ) { Build( "do_code_gen", filename ); } [TestMethod] public void AdditionalFiles() { List SyntaxTree = new List(); var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) .WithConcurrentBuild( true ) .WithOptimizationLevel( OptimizationLevel.Debug ) .WithGeneralDiagnosticOption( ReportDiagnostic.Info ) .WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu ) .WithAllowUnsafe( false ); var refs = new List(); var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location ); refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) ); refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); CSharpCompilation compiler = CSharpCompilation.Create( $"poopy.dll", SyntaxTree, refs, optn ); // Process Razor files using RazorProcessor before running the Processor foreach ( var file in System.IO.Directory.EnumerateFiles( "data/codegen/", "*.razor" ) ) { var razorText = System.IO.File.ReadAllText( file ); var generatedCode = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, file ); var razorTree = CSharpSyntaxTree.ParseText( generatedCode, path: $"_gen_{System.IO.Path.GetFileName( file )}.cs", encoding: System.Text.Encoding.UTF8 ); compiler = compiler.AddSyntaxTrees( razorTree ); } var processor = new Sandbox.Generator.Processor(); processor.AddonName = $"poopy"; processor.Run( compiler ); compiler = processor.Compilation; foreach ( var tree in compiler.SyntaxTrees ) { System.Console.WriteLine( tree.GetText().ToString() ); } } /// /// Note that our aim here is to prevent 99% of accidental overflows. There are still going to be ways /// to overflow and force quit.. but this should all stop the accidental stuff. /// // [TestMethod] public void StackOverflowPrevention() { var compiler = Build( "do_code_gen", "TestStackOverflow.cs" ); // actually compile using var stream = new MemoryStream(); var result = compiler.Emit( stream ); Assert.IsTrue( result.Success, "Failed to compile!" ); // Load it var assemblyBytes = stream.ToArray(); var asm = System.Reflection.Assembly.Load( assemblyBytes ); // Call RecurseForever var TestStackOverflow = asm.GetType( "TestStackOverflow" ); // Should successfully throw an exception due to stack overflowing var RecurseForever = TestStackOverflow.GetMethod( "RecurseForever" ); Assert.ThrowsException( () => RecurseForever.Invoke( null, null ) ); var RecurseForeverInline = TestStackOverflow.GetMethod( "RecurseForeverInline" ); Assert.ThrowsException( () => RecurseForeverInline.Invoke( null, null ) ); } [TestMethod] public void TestCloudAssets() { var compiler = Build( "do_code_gen", "CloudAssetTest.cs" ); Assert.AreEqual( 2, compiler.SyntaxTrees.Count(), "Should have the original syntaxtree plus the generated one" ); var added = compiler.SyntaxTrees.First( x => x.FilePath.Contains( "_gen__AddedCode.cs" ) ); Assert.IsTrue( added.GetText().ToString().Contains( "[assembly:Sandbox.Cloud.Asset( " ), "Outputted text should contain the asset attribute" ); } [TestMethod] public void TestWrapCall() { var compiler = Build( "do_code_gen", "TestWrapCall.cs" ); var tree = compiler.SyntaxTrees.First(); System.Console.WriteLine( tree.GetText().ToString() ); Assert.IsTrue( tree.GetText().ToString().Contains( "WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod {Resume = () => {},Object = null,MethodIdentity = -1168963981,MethodName = \"TestWrappedStaticCall\",TypeName = \"TestWrapCall\",IsStatic = true,Attributes = __m_1168963981__Attrs}, arga );" ), "Generated code should wrap static method call" ); Assert.IsTrue( tree.GetText().ToString().Contains( "OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => {},Object = this,MethodIdentity = -446800946,MethodName = \"TestWrappedInstanceCall\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_446800946__Attrs}, arga );" ), "Generated code should wrap instance method call" ); Assert.IsTrue( tree.GetText().ToString().Contains( "WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod {Resume = () => {},Object = null,MethodIdentity = 1638661065,MethodName = \"TestWrappedStaticCallNoArg\",TypeName = \"TestWrapCall\",IsStatic = true,Attributes = __1638661065__Attrs} );" ), "Generated code should wrap static method call with no arg" ); Assert.IsTrue( tree.GetText().ToString().Contains( "OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => {},Object = this,MethodIdentity = -1769572979,MethodName = \"TestWrappedInstanceCallNoArg\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_1769572979__Attrs} );" ), "Generated code should wrap instance method call with no arg" ); Assert.IsTrue( tree.GetText().ToString().Contains( "public void ExpressionBodiedBroadcast() => OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => Log.Info( \"Test.\" ),Object = this,MethodIdentity = 1201362747,MethodName = \"ExpressionBodiedBroadcast\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __1201362747__Attrs} );" ), "Generated code should wrap expression bodied method" ); Assert.IsTrue( tree.GetText().ToString().Contains( "Object = this,MethodIdentity = -1316352073,MethodName = \"TestWrappedInstanceCallReturnType\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_1316352073__Attrs}, arga );" ), "Generated code should wrap instance method call with return type" ); Assert.IsTrue( tree.GetText().ToString().Contains( "return WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod> {Resume = async () =>" ), "Generated code should wrap async Task method call" ); } [TestMethod] public void TestPreprocessorWrappedCall() { var compiler = Build( "do_code_gen", "TestPreprocessorWrapCall.cs" ); var tree = compiler.SyntaxTrees.First(); var treeText = tree.GetText().ToString(); Assert.IsTrue( treeText.Contains( "{\r\n#if true\r\n\t\tif ( true )\r\n\t\t{\r\n\t\t\t// Hello there!\r\n\t\t}\r\n#endif\r\n\t}" ), "Maintain preprocessor directive in body" ); Assert.IsFalse( treeText.Contains( $");}}#endif" ), "No trailing trivia outside of body" ); } [TestMethod] public void TestWrapGet() { var compiler = Build( "do_code_gen", "TestWrapGet.cs" ); var tree = compiler.SyntaxTrees.First(); System.Console.WriteLine( tree.GetText().ToString() ); Assert.IsTrue( tree.GetText().ToString().Contains( "return (bool)WrapGet.OnWrapGetStatic(new global::Sandbox.WrappedPropertyGet { Value = value, Object = null, IsStatic = true, TypeName = \"TestWrapGet\", PropertyName = \"StaticProperty\", MemberIdent = 2112852062, Attributes = __StaticProperty__Attrs });" ), "Generated code should wrap static property get accessor" ); Assert.IsTrue( tree.GetText().ToString().Contains( "return (bool)OnWrapGet(new global::Sandbox.WrappedPropertyGet { Value = value, Object = this, IsStatic = false, TypeName = \"TestWrapGet\", PropertyName = \"InstanceProperty\", MemberIdent = 1816497229, Attributes = __InstanceProperty__Attrs });" ), "Generated code should wrap instance property get accessor" ); Assert.IsTrue( tree.GetText().ToString().Contains( "_repback__InstanceProperty= true;" ), "Generated code should copy initializer" ); } [TestMethod] public void TestWrapSet() { var compiler = Build( "do_code_gen", "TestWrapSet.cs" ); var tree = compiler.SyntaxTrees.First(); System.Console.WriteLine( tree.GetText().ToString() ); Assert.IsTrue( tree.GetText().ToString().Contains( "WrapSet.OnWrapSetStatic(new global::Sandbox.WrappedPropertySet { Value = value, Object = null, Setter = (v) =>" ), "Generated code should wrap static property set accessor" ); Assert.IsTrue( tree.GetText().ToString().Contains( "OnWrapSet(new global::Sandbox.WrappedPropertySet { Value = value, Object = this, Setter = (v) =>" ), "Generated code should wrap instance property set accessor" ); } [TestMethod] public void CodeGenFullyQualified() { var compiler = BuildString( """ namespace CodeGenFullyQualified; [Sandbox.CodeGenerator(Sandbox.CodeGeneratorFlags.WrapPropertyGet | Sandbox.CodeGeneratorFlags.Static | Sandbox.CodeGeneratorFlags.Instance, "CodeGenFullyQualified.TestCodeGenAttribute.GetWrapper")] [System.AttributeUsage(System.AttributeTargets.Property)] public class TestCodeGenAttribute : System.Attribute { internal static T GetWrapper(Sandbox.WrappedPropertyGet property) { return property.Value; } } public class Program { [TestCodeGen] public bool TestProperty { get; set; } public static void Main() { } } """ ); var tree = compiler.SyntaxTrees.First(); Assert.IsTrue( compiler.GetDiagnostics().Length == 0, "Should be no compile errors" ); } [TestMethod] public void StringTokenReplacement() { var compiler = BuildString( """" using Sandbox; public static class Program { public static void Main() { RenderAttributes ra = new (); ra.Set( "PoopSock", Vector3.One ); } } """" ); var tree = compiler.SyntaxTrees.First(); Assert.IsTrue( tree.GetText().ToString().Contains( "\"PoopSock\",1590150592U" ), "Should have replaced string token" ); } [TestMethod] public void ParameterDescriptionAttribute() { var compiler = Build( "do_code_gen", "Descriptions.cs" ); var tree = compiler.SyntaxTrees.First(); Assert.IsTrue( tree.GetText().ToString().Contains( "[DescriptionAttribute( \"This parameter has a description!\" )]int baz" ) ); } [TestMethod] public void ReturnsDescriptionAttribute() { var compiler = Build( "do_code_gen", "Descriptions.cs" ); var tree = compiler.SyntaxTrees.First(); Assert.IsTrue( tree.GetText().ToString().Contains( "[return:DescriptionAttribute( \"Here's a description of the return value!\" )]" ) ); } } }