Browse Source

Add: Lua.SourceGenerator

AnnulusGames 1 year ago
parent
commit
490d4b1f7b

+ 7 - 0
Lua.sln

@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "sandbox\Cons
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "sandbox\Benchmark\Benchmark.csproj", "{FC157C29-8AAE-49C8-9536-208E3F0698DA}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lua.SourceGenerator", "src\Lua.SourceGenerator\Lua.SourceGenerator.csproj", "{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -42,11 +44,16 @@ Global
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D} = {18A64E25-9557-457B-80AE-A6EFE853118D}
 		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36} = {79458370-DD8A-48A4-B11E-8DF631520E8C}
 		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4} = {18A64E25-9557-457B-80AE-A6EFE853118D}
 	EndGlobalSection
 EndGlobal

+ 7 - 0
sandbox/ConsoleApp1/ConsoleApp1.csproj

@@ -2,6 +2,10 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\src\Lua\Lua.csproj" />
+    <ProjectReference Include="..\..\src\Lua.SourceGenerator\Lua.SourceGenerator.csproj">
+			<OutputItemType>Analyzer</OutputItemType>
+			<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+		</ProjectReference>
   </ItemGroup>
 
   <PropertyGroup>
@@ -9,6 +13,9 @@
     <TargetFramework>net8.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
+
+    <EmitCompilerGeneratedFiles>false</EmitCompilerGeneratedFiles>
+    <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
   </PropertyGroup>
 
 </Project>

+ 25 - 0
sandbox/ConsoleApp1/LVec3.cs

@@ -0,0 +1,25 @@
+using Lua;
+
+[LuaObject("vec3")]
+public partial class LVec3
+{
+    [LuaMember("x")]
+    public double X { get; set; }
+
+    [LuaMember("y")]
+    public double Y { get; set; }
+
+    [LuaMember("z")]
+    public double Z { get; set; }
+
+    [LuaMember("create")]
+    public static LVec3 Create(double x, double y, double z)
+    {
+        return new LVec3()
+        {
+            X = x,
+            Y = y,
+            Z = z,
+        };
+    }
+}

+ 0 - 58
sandbox/ConsoleApp1/vec3.lua

@@ -1,58 +0,0 @@
-Vec3 = {
-    new = function(x, y, z)
-        local instance = { x, y, z }
-        setmetatable(instance, Vec3)
-        return instance
-    end,
-
-    magnitude = function(self)
-        return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
-    end,
-
-    __index = function(table, index)
-        if index == 'x' then
-            return rawget(table, 1)
-        elseif index == 'y' then
-            return rawget(table, 2)
-        elseif index == 'z' then
-            return rawget(table, 3)
-        else
-            error('vec3 key must be x, y or z')
-        end
-    end,
-
-    __newindex = function(table, index, value)
-        if index == 'x' then
-            return rawset(table, 1, value)
-        elseif index == 'y' then
-            return rawset(table, 2, value)
-        elseif index == 'z' then
-            return rawset(table, 3, value)
-        else
-            error('vec3 key must be x, y or z')
-        end
-    end,
-
-    __add = function(a, b)
-        return Vec3.new(a.x + b.x, a.y + b.y, a.z + b.z)
-    end,
-
-    __sub = function(a, b)
-        return Vec3.new(a.x - b.x, a.y - b.y, a.z - b.z)
-    end,
-
-    __unm = function(a)
-        return Vec3.new(-a.x, -a.y, -a.z)
-    end,
-
-    __eq = function(a, b)
-        return a.x == b.y and a.y == b.y and a.z == b.z
-    end,
-
-    __tostring = function(self)
-        return '(' .. self.x .. ',' .. self.y .. ',' .. self.z .. ')'
-    end
-}
-
-local a = Vec3.new(1, 1, 1)
-local b = Vec3.new(1, 1, 1)

+ 18 - 0
src/Lua.SourceGenerator/Comparer.cs

@@ -0,0 +1,18 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+internal sealed class Comparer : IEqualityComparer<(GeneratorAttributeSyntaxContext, Compilation)>
+{
+    public static readonly Comparer Instance = new();
+
+    public bool Equals((GeneratorAttributeSyntaxContext, Compilation) x, (GeneratorAttributeSyntaxContext, Compilation) y)
+    {
+        return x.Item1.TargetNode.Equals(y.Item1.TargetNode);
+    }
+
+    public int GetHashCode((GeneratorAttributeSyntaxContext, Compilation) obj)
+    {
+        return obj.Item1.TargetNode.GetHashCode();
+    }
+}

+ 40 - 0
src/Lua.SourceGenerator/DiagnosticDescriptors.cs

@@ -0,0 +1,40 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+public static class DiagnosticDescriptors
+{
+    const string Category = "Lua";
+
+    public static readonly DiagnosticDescriptor MustBePartial = new(
+        id: "LUACS001",
+        title: "LuaObject type must be partial.",
+        category: Category,
+        messageFormat: "LuaObject type '{0}' must be partial",
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor NestedNotAllowed = new(
+        id: "LUACS002",
+        title: "LuaObject type must not be nested",
+        messageFormat: "LuaObject type '{0}' must be not nested",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor AbstractNotAllowed = new(
+        id: "LUAC003",
+        title: "LuaObject type must not abstract",
+        messageFormat: "LuaObject object '{0}' must be not abstract",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor InvalidPropertyType = new(
+        id: "LUAC004",
+        title: "The type of the field or property must be LuaValue or a type that can be converted to LuaValue.",
+        messageFormat: "The type of the field or property must be LuaValue or a type that can be converted to LuaValue.",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+}

+ 23 - 0
src/Lua.SourceGenerator/Lua.SourceGenerator.csproj

@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>12</LangVersion>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <IsRoslynComponent>true</IsRoslynComponent>
+    <AnalyzerLanguage>cs</AnalyzerLanguage>
+    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+    <IncludeBuildOutput>false</IncludeBuildOutput>
+    <DevelopmentDependency>true</DevelopmentDependency>
+    <IncludeSymbols>false</IncludeSymbols>
+    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" />
+	
+	  <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+  </ItemGroup>
+
+</Project>

+ 214 - 0
src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs

@@ -0,0 +1,214 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+partial class LuaObjectGenerator
+{
+    static bool TryEmit(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context)
+    {
+        try
+        {
+            var error = false;
+
+            // must be partial
+            if (!typeMetadata.IsPartial())
+            {
+                context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.MustBePartial,
+                    typeMetadata.Syntax.Identifier.GetLocation(),
+                    typeMetadata.Symbol.Name));
+                error = true;
+            }
+
+            // nested is not allowed
+            if (typeMetadata.IsNested())
+            {
+                context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.NestedNotAllowed,
+                    typeMetadata.Syntax.Identifier.GetLocation(),
+                    typeMetadata.Symbol.Name));
+                error = true;
+            }
+
+            // verify abstract/interface
+            if (typeMetadata.Symbol.IsAbstract)
+            {
+                context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.AbstractNotAllowed,
+                    typeMetadata.Syntax.Identifier.GetLocation(),
+                    typeMetadata.TypeName));
+                error = true;
+            }
+
+            if (error)
+            {
+                return false;
+            }
+
+            builder.AppendLine("// <auto-generated />");
+            builder.AppendLine("#nullable enable");
+            builder.AppendLine("#pragma warning disable CS0162 // Unreachable code");
+            builder.AppendLine("#pragma warning disable CS0219 // Variable assigned but never used");
+            builder.AppendLine("#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.");
+            builder.AppendLine("#pragma warning disable CS8601 // Possible null reference assignment");
+            builder.AppendLine("#pragma warning disable CS8602 // Possible null return");
+            builder.AppendLine("#pragma warning disable CS8604 // Possible null reference argument for parameter");
+            builder.AppendLine("#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method");
+            builder.AppendLine();
+
+            var ns = typeMetadata.Symbol.ContainingNamespace;
+            if (!ns.IsGlobalNamespace)
+            {
+                builder.AppendLine($"namespace {ns}");
+                builder.BeginBlock();
+            }
+
+            var typeDeclarationKeyword = (typeMetadata.Symbol.IsRecord, typeMetadata.Symbol.IsValueType) switch
+            {
+                (true, true) => "record struct",
+                (true, false) => "record",
+                (false, true) => "struct",
+                (false, false) => "class",
+            };
+
+            using var _ = builder.BeginBlockScope($"partial {typeDeclarationKeyword} {typeMetadata.TypeName} : global::Lua.ILuaUserData");
+
+            // add ILuaUserData impl
+            builder.Append(
+"""
+        global::Lua.LuaTable? global::Lua.ILuaUserData.Metatable
+        {
+            get
+            {
+                if (__metatable != null) return __metatable;
+
+                __metatable = new();
+                __metatable[global::Lua.Runtime.Metamethods.Index] = __metamethod_index;
+                __metatable[global::Lua.Runtime.Metamethods.NewIndex] = __metamethod_newindex;
+                return __metatable;
+            }
+            set
+            {
+                __metatable = value;
+            }
+        }
+        static global::Lua.LuaTable? __metatable;
+
+""", false);
+
+            if (!TryEmitIndexMetamethod(typeMetadata, builder, context))
+            {
+                return false;
+            }
+
+            if (!TryEmitNewIndexMetamethod(typeMetadata, builder, context))
+            {
+                return false;
+            }
+
+            if (!ns.IsGlobalNamespace) builder.EndBlock();
+
+            builder.AppendLine("#pragma warning restore CS0162 // Unreachable code");
+            builder.AppendLine("#pragma warning restore CS0219 // Variable assigned but never used");
+            builder.AppendLine("#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.");
+            builder.AppendLine("#pragma warning restore CS8601 // Possible null reference assignment");
+            builder.AppendLine("#pragma warning restore CS8602 // Possible null return");
+            builder.AppendLine("#pragma warning restore CS8604 // Possible null reference argument for parameter");
+            builder.AppendLine("#pragma warning restore CS8631 // The type cannot be used as type parameter in the generic type or method");
+            return true;
+        }
+        catch (Exception)
+        {
+            return false;
+        }
+    }
+
+    static bool TryEmitIndexMetamethod(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context)
+    {
+        builder.AppendLine("static readonly global::Lua.LuaFunction __metamethod_index = new global::Lua.LuaFunction((context, buffer, ct) =>");
+
+        using (builder.BeginBlockScope())
+        {
+            builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);");
+            builder.AppendLine($"var key = context.GetArgument<global::System.String>(1);");
+            builder.AppendLine("var result = key switch");
+
+            using (builder.BeginBlockScope())
+            {
+                foreach (var propertyMetadata in typeMetadata.Properties)
+                {
+                    if (propertyMetadata.IsStatic)
+                    {
+                        builder.AppendLine(@$"""{propertyMetadata.LuaMemberName}"" => new global::Lua.LuaValue({typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name}),");
+                    }
+                    else
+                    {
+                        builder.AppendLine(@$"""{propertyMetadata.LuaMemberName}"" => new global::Lua.LuaValue(userData.{propertyMetadata.Symbol.Name}),");
+                    }
+                }
+
+                builder.AppendLine(@$"_ => throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""{typeMetadata.LuaTypeName}.{{key}} not found.""),");
+            }
+            builder.AppendLine(";");
+
+            builder.AppendLine("buffer.Span[0] = result;");
+            builder.AppendLine("return new(1);");
+        }
+
+        builder.AppendLine(");");
+
+        return true;
+    }
+
+    static bool TryEmitNewIndexMetamethod(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context)
+    {
+        builder.AppendLine("static readonly global::Lua.LuaFunction __metamethod_newindex = new global::Lua.LuaFunction((context, buffer, ct) =>");
+
+        using (builder.BeginBlockScope())
+        {
+            builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);");
+            builder.AppendLine($"var key = context.GetArgument<global::System.String>(1);");
+            builder.AppendLine("switch (key)");
+
+            using (builder.BeginBlockScope())
+            {
+                foreach (var propertyMetadata in typeMetadata.Properties)
+                {
+                    builder.AppendLine(@$"case ""{propertyMetadata.LuaMemberName}"":");
+
+                    using (builder.BeginIndentScope())
+                    {
+                        if (propertyMetadata.IsReadOnly)
+                        {
+                            builder.AppendLine($@"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""{typeMetadata.LuaTypeName}.{{key}}cannot overwrite."");");
+                        }
+                        else if (propertyMetadata.IsStatic)
+                        {
+                            builder.AppendLine(@$"{typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);");
+                            builder.AppendLine("break;");
+                        }
+                        else
+                        {
+                            builder.AppendLine(@$"userData.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);");
+                            builder.AppendLine("break;");
+                        }
+                    }
+                }
+
+                builder.AppendLine(@$"default:");
+
+                using (builder.BeginIndentScope())
+                {
+                    builder.AppendLine(@$"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""{typeMetadata.LuaTypeName}.{{key}} not found."");");
+                }
+            }
+
+            builder.AppendLine("return new(0);");
+        }
+
+        builder.AppendLine(");");
+
+        return true;
+    }
+
+}

+ 54 - 0
src/Lua.SourceGenerator/LuaObjectGenerator.cs

@@ -0,0 +1,54 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lua.SourceGenerator;
+
+[Generator(LanguageNames.CSharp)]
+public partial class LuaObjectGenerator : IIncrementalGenerator
+{
+    public void Initialize(IncrementalGeneratorInitializationContext context)
+    {
+        var provider = context.SyntaxProvider
+            .ForAttributeWithMetadataName(
+                "Lua.LuaObjectAttribute",
+                static (node, cancellation) =>
+                {
+                    return node is ClassDeclarationSyntax
+                        or RecordDeclarationSyntax;
+                },
+                static (context, cancellation) => { return context; })
+            .Combine(context.CompilationProvider)
+            .WithComparer(Comparer.Instance);
+
+        context.RegisterSourceOutput(
+            context.CompilationProvider.Combine(provider.Collect()),
+            (sourceProductionContext, t) =>
+            {
+                var (compilation, list) = t;
+                var references = SymbolReferences.Create(compilation);
+                if (references == null) return;
+
+                var builder = new CodeBuilder();
+
+                var targetTypes = new List<TypeMetadata>();
+
+                foreach (var (x, _) in list)
+                {
+                    var typeMeta = new TypeMetadata((TypeDeclarationSyntax)x.TargetNode, (INamedTypeSymbol)x.TargetSymbol, references);
+
+                    if (TryEmit(typeMeta, builder, in sourceProductionContext))
+                    {
+                        var fullType = typeMeta.FullTypeName
+                            .Replace("global::", "")
+                            .Replace("<", "_")
+                            .Replace(">", "_");
+
+                        sourceProductionContext.AddSource($"{fullType}.LuaObject.g.cs", builder.ToString());
+                        targetTypes.Add(typeMeta);
+                    }
+
+                    builder.Clear();
+                }
+            });
+    }
+}

+ 31 - 0
src/Lua.SourceGenerator/MethodMetadata.cs

@@ -0,0 +1,31 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+public class MethodMetadata
+{
+    public IMethodSymbol Symbol { get; }
+    public bool IsStatic { get; }
+    public string LuaMemberName { get; }
+
+    public MethodMetadata(IMethodSymbol symbol, SymbolReferences references)
+    {
+        Symbol = symbol;
+        IsStatic = symbol.IsStatic;
+
+        LuaMemberName = symbol.Name;
+
+        var memberAttribute = symbol.GetAttribute(references.LuaMemberAttribute);
+        if (memberAttribute != null)
+        {
+            if (memberAttribute.ConstructorArguments.Length > 0)
+            {
+                var value = memberAttribute.ConstructorArguments[0].Value;
+                if (value is string str)
+                {
+                    LuaMemberName = str;
+                }
+            }
+        }
+    }
+}

+ 49 - 0
src/Lua.SourceGenerator/PropertyMetadata.cs

@@ -0,0 +1,49 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+public class PropertyMetadata
+{
+    public ISymbol Symbol { get; }
+    public string TypeFullName { get; }
+    public bool IsStatic { get; }
+    public bool IsReadOnly { get; }
+    public string LuaMemberName { get; }
+
+    public PropertyMetadata(ISymbol symbol, SymbolReferences references)
+    {
+        Symbol = symbol;
+
+        IsStatic = symbol.IsStatic;
+
+        if (symbol is IFieldSymbol field)
+        {
+            TypeFullName = field.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+            IsReadOnly = field.IsReadOnly;
+        }
+        else if (symbol is IPropertySymbol property)
+        {
+            TypeFullName = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+            IsReadOnly = property.SetMethod == null;
+        }
+        else
+        {
+            TypeFullName = "";
+        }
+
+        LuaMemberName = symbol.Name;
+
+        var memberAttribute = symbol.GetAttribute(references.LuaMemberAttribute);
+        if (memberAttribute != null)
+        {
+            if (memberAttribute.ConstructorArguments.Length > 0)
+            {
+                var value = memberAttribute.ConstructorArguments[0].Value;
+                if (value is string str)
+                {
+                    LuaMemberName = str;
+                }
+            }
+        }
+    }
+}

+ 23 - 0
src/Lua.SourceGenerator/SymbolReferences.cs

@@ -0,0 +1,23 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+public sealed class SymbolReferences
+{
+    public static SymbolReferences? Create(Compilation compilation)
+    {
+        var luaObjectAttribute = compilation.GetTypeByMetadataName("Lua.LuaObjectAttribute");
+        if (luaObjectAttribute == null) return null;
+
+        return new SymbolReferences
+        {
+            LuaObjectAttribute = luaObjectAttribute,
+            LuaMemberAttribute = compilation.GetTypeByMetadataName("Lua.LuaMemberAttribute")!,
+            LuaIgnoreMemberAttribute = compilation.GetTypeByMetadataName("Lua.LuaIgnoreMemberAttribute")!,
+        };
+    }
+
+    public INamedTypeSymbol LuaObjectAttribute { get; private set; } = default!;
+    public INamedTypeSymbol LuaMemberAttribute { get; private set; } = default!;
+    public INamedTypeSymbol LuaIgnoreMemberAttribute { get; private set; } = default!;
+}

+ 72 - 0
src/Lua.SourceGenerator/TypeMetadata.cs

@@ -0,0 +1,72 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lua.SourceGenerator;
+
+internal record class TypeMetadata
+{
+    public TypeDeclarationSyntax Syntax { get; }
+    public INamedTypeSymbol Symbol { get; }
+    public string TypeName { get; }
+    public string FullTypeName { get; }
+    public string LuaTypeName { get; }
+    public PropertyMetadata[] Properties { get; }
+    public MethodMetadata[] Methods { get; }
+
+    public TypeMetadata(TypeDeclarationSyntax syntax, INamedTypeSymbol symbol, SymbolReferences references)
+    {
+        Syntax = syntax;
+        Symbol = symbol;
+
+        TypeName = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
+        FullTypeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+        var objectAttribute = symbol.GetAttributes().FindAttributeShortName("LuaObjectAttribute")!;
+
+        LuaTypeName = objectAttribute.ConstructorArguments.Length > 0
+            ? (string)objectAttribute.ConstructorArguments[0].Value!
+            : symbol.Name;
+
+        Properties = Symbol.GetAllMembers()
+            .Where(x => x is (IFieldSymbol or IPropertySymbol) and { IsImplicitlyDeclared: false })
+            .Where(x =>
+            {
+                if (!x.ContainsAttribute(references.LuaMemberAttribute)) return false;
+                if (x.ContainsAttribute(references.LuaIgnoreMemberAttribute)) return false;
+
+                if (x is IPropertySymbol p)
+                {
+                    if (p.GetMethod == null || p.SetMethod == null) return false;
+                    if (p.IsIndexer) return false;
+                }
+
+                return true;
+            })
+            .Select(x => new PropertyMetadata(x, references))
+            .ToArray();
+
+        Methods = Symbol.GetAllMembers()
+            .Where(x => x is IMethodSymbol and { IsImplicitlyDeclared: false })
+            .Select(x => (IMethodSymbol)x)
+            .Where(x =>
+            {
+                if (!x.ContainsAttribute(references.LuaMemberAttribute)) return false;
+                if (x.ContainsAttribute(references.LuaIgnoreMemberAttribute)) return false;
+
+                return true;
+            })
+            .Select(x => new MethodMetadata(x, references))
+            .ToArray();
+    }
+
+    public bool IsPartial()
+    {
+        return Syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
+    }
+
+    public bool IsNested()
+    {
+        return Syntax.Parent is TypeDeclarationSyntax;
+    }
+}

+ 120 - 0
src/Lua.SourceGenerator/Utilities/CodeBuilder.cs

@@ -0,0 +1,120 @@
+using System.Text;
+
+namespace Lua.SourceGenerator;
+
+internal sealed class CodeBuilder
+{
+    public ref struct IndentScope
+    {
+        readonly CodeBuilder source;
+
+        public IndentScope(CodeBuilder source, string? startLine = null)
+        {
+            this.source = source;
+            source.AppendLine(startLine);
+            source.IncreaseIndent();
+        }
+
+        public void Dispose()
+        {
+            source.DecreaseIndent();
+        }
+    }
+
+    public ref struct BlockScope
+    {
+        readonly CodeBuilder source;
+
+        public BlockScope(CodeBuilder source, string? startLine = null)
+        {
+            this.source = source;
+            source.AppendLine(startLine);
+            source.BeginBlock();
+        }
+
+        public void Dispose()
+        {
+            source.EndBlock();
+        }
+    }
+
+    readonly StringBuilder buffer = new();
+    int indentLevel;
+
+    public IndentScope BeginIndentScope(string? startLine = null) => new(this, startLine);
+    public BlockScope BeginBlockScope(string? startLine = null) => new(this, startLine);
+
+    public void Append(string value, bool indent = true)
+    {
+        if (indent)
+        {
+            buffer.Append($"{new string(' ', indentLevel * 4)} {value}");
+        }
+        else
+        {
+            buffer.Append(value);
+        }
+    }
+
+    public void AppendLine(string? value = null, bool indent = true)
+    {
+        if (string.IsNullOrEmpty(value))
+        {
+            buffer.AppendLine();
+        }
+        else if (indent)
+        {
+            buffer.AppendLine($"{new string(' ', indentLevel * 4)} {value}");
+        }
+        else
+        {
+            buffer.AppendLine(value);
+        }
+    }
+
+    public void AppendByteArrayString(byte[] bytes)
+    {
+        buffer.Append("{ ");
+        var first = true;
+        foreach (var x in bytes)
+        {
+            if (!first)
+            {
+                buffer.Append(", ");
+            }
+            buffer.Append(x);
+            first = false;
+        }
+        buffer.Append(" }");
+    }
+
+    public override string ToString() => buffer.ToString();
+
+    public void IncreaseIndent()
+    {
+        indentLevel++;
+    }
+
+    public void DecreaseIndent()
+    {
+        if (indentLevel > 0)
+            indentLevel--;
+    }
+
+    public void BeginBlock()
+    {
+        AppendLine("{");
+        IncreaseIndent();
+    }
+
+    public void EndBlock()
+    {
+        DecreaseIndent();
+        AppendLine("}");
+    }
+
+    public void Clear()
+    {
+        buffer.Clear();
+    }
+}

+ 20 - 0
src/Lua.SourceGenerator/Utilities/RoslynAnalyzerExtensions.cs

@@ -0,0 +1,20 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+internal static class RoslynAnalyzerExtensions
+{
+    public static AttributeData? FindAttribute(this IEnumerable<AttributeData> attributeDataList, string typeName)
+    {
+        return attributeDataList
+            .Where(x => x.AttributeClass?.ToDisplayString() == typeName)
+            .FirstOrDefault();
+    }
+
+    public static AttributeData? FindAttributeShortName(this IEnumerable<AttributeData> attributeDataList, string typeName)
+    {
+        return attributeDataList
+            .Where(x => x.AttributeClass?.Name == typeName)
+            .FirstOrDefault();
+    }
+}

+ 40 - 0
src/Lua.SourceGenerator/Utilities/SymbolExtensions.cs

@@ -0,0 +1,40 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+internal static class SymbolExtensions
+{
+    public static bool ContainsAttribute(this ISymbol symbol, INamedTypeSymbol attribtue)
+    {
+        return symbol.GetAttributes().Any(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, attribtue));
+    }
+
+    public static AttributeData? GetAttribute(this ISymbol symbol, INamedTypeSymbol attribtue)
+    {
+        return symbol.GetAttributes().FirstOrDefault(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, attribtue));
+    }
+
+    public static IEnumerable<ISymbol> GetAllMembers(this INamedTypeSymbol symbol, bool withoutOverride = true)
+    {
+        // Iterate Parent -> Derived
+        if (symbol.BaseType != null)
+        {
+            foreach (var item in GetAllMembers(symbol.BaseType))
+            {
+                // override item already iterated in parent type
+                if (!withoutOverride || !item.IsOverride)
+                {
+                    yield return item;
+                }
+            }
+        }
+
+        foreach (var item in symbol.GetMembers())
+        {
+            if (!withoutOverride || !item.IsOverride)
+            {
+                yield return item;
+            }
+        }
+    }
+}

+ 36 - 0
src/Lua/Attributes.cs

@@ -0,0 +1,36 @@
+namespace Lua;
+
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class LuaObjectAttribute : Attribute
+{
+    public LuaObjectAttribute()
+    {
+    }
+
+    public LuaObjectAttribute(string name)
+    {
+        Name = name;
+    }
+
+    public string? Name { get; }
+}
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class LuaMemberAttribute : Attribute
+{
+    public LuaMemberAttribute()
+    {
+    }
+
+    public LuaMemberAttribute(string name)
+    {
+        Name = name;
+    }
+
+    public string? Name { get; }
+}
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class LuaIgnoreMemberAttribute : Attribute
+{
+}

+ 12 - 1
src/Lua/Lua.csproj

@@ -18,6 +18,11 @@
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>
+
+    <None Include="..\Lua.SourceGenerator\bin\$(Configuration)\netstandard2.0\Lua.SourceGenerator.dll"
+          PackagePath="analyzers\dotnet\cs"
+          Pack="true"
+          Visible="false" />
   </ItemGroup>
 
   <ItemGroup Condition="$(TargetFramework) == 'netstandard2.1'">
@@ -26,8 +31,14 @@
   
   <ItemGroup>
       <None Include="../../Icon.png" Pack="true" PackagePath="/" />
-      <None Include="..\..\README.md" Pack="true" PackagePath="README.md"/>
+      <None Include="..\..\README.md" Pack="true" PackagePath="README.md" />
       <EmbeddedResource Include="..\..\LICENSE" />
   </ItemGroup>
+  
+  <ItemGroup>
+    <ProjectReference Include="..\Lua.SourceGenerator\Lua.SourceGenerator.csproj"
+      OutputItemType="Analyzer"
+      ReferenceOutputAssembly="false"/>
+  </ItemGroup>
 
 </Project>