From 3ef4d8c00fece3318833970d814fcefd5706989f Mon Sep 17 00:00:00 2001
From: trippwill <5862883+trippwill@users.noreply.github.com>
Date: Mon, 24 Feb 2025 00:22:55 +0000
Subject: [PATCH 01/11] Generator Epic: Major Refactor
---
.editorconfig | 15 +-
Directory.Packages.props | 1 +
.../AccessibilityMode.cs} | 11 +-
.../DictionaryKeyMode.cs | 13 +-
.../EnumMode.cs} | 9 +-
.../GeneratorOptions.cs | 29 ++
.../SchemaAccessibilityModeAttribute.cs | 21 ++
.../SchemaDictionaryKeyModeAttribute.cs | 21 ++
.../SchemaEnumModeAttribute.cs | 21 ++
.../SchemaTraversalModeAttribute.cs | 21 ++
.../SharpSchema.Annotations.csproj | 6 +
.../TraversalMode.cs} | 11 +-
src/SharpSchema.Generator/GeneratorOptions.cs | 20 -
.../LeafDeclaredTypeSyntaxVisitor.cs | 355 ------------------
.../LeafNodeSymbolVisitor.cs | 0
.../LeafSyntaxVisitor.cs | 349 +++++++++++++++++
src/SharpSchema.Generator/Model/Metadata.cs | 32 +-
.../Resolvers/CollectionResolver.cs | 72 ++++
.../EnumSymbolVisitor.cs | 11 +-
...eSyntaxVisitor.cs => RootSyntaxVisitor.cs} | 10 +-
.../SharpSchema.Generator.csproj | 4 -
.../TypeSchemaSymbolVisitor.cs | 193 ++++++++++
.../Utilities/AttributeDataExtensions.cs | 25 --
.../Utilities/AttributeHandler.cs | 63 ++++
.../Utilities/CollectionExtensions.cs | 16 +
.../Utilities/CollectionSymbolVisitor.cs | 98 -----
.../Utilities/CompilationExtensions.cs | 29 ++
.../Utilities/EnumExtensions.cs | 5 +-
.../Utilities/GeneratorOptionsExtensions.cs | 11 +-
.../Utilities/JsonSchemaBuilderExtensions.cs | 36 +-
.../Utilities/SymbolExtensions.cs | 82 ++--
.../Utilities/SyntaxExtensions.cs | 111 +-----
src/SharpSchema.Generator/Utilities/Tracer.cs | 25 +-
.../AllFeaturesProjectTests.cs | 7 +-
...Record_WithAbstractParameters.verified.txt | 114 ------
...Mode_dictionaryKeyMode=Strict.verified.txt | 13 -
...mHandling_enumHandling=String.verified.txt | 114 ------
...g_enumHandling=UnderlyingType.verified.txt | 110 ------
.../TestData.cs | 139 +++++--
.../TestDataFixture.cs | 8 +-
...ties_accessibilities=Internal.verified.txt | 0
...ities_accessibilities=Private.verified.txt | 0
...lities_accessibilities=Public.verified.txt | 0
...ccessibilities=PublicInternal.verified.txt | 0
...me=Class_ExtendsAbstractClass.verified.txt | 0
...ame=Class_WithArrayProperties.verified.txt | 4 +-
...lass_WithDictionaryProperties.verified.txt | 4 +-
...estName=Class_WithDocComments.verified.txt | 3 +-
...ame=Class_WithIgnoredProperty.verified.txt | 0
...=Class_WithInternalProperties.verified.txt | 0
...e=Class_WithInvalidProperties.verified.txt | 3 +-
...Name=Class_WithSchemaOverride.verified.txt | 0
...=Class_WithTypeSchemaOverride.verified.txt | 3 +-
..._WithUnsupportedDictionaryKey.verified.txt | 5 +-
...testName=Class_WithValueTypes.verified.txt | 3 +-
...aultOptions_testName=GameHall.verified.txt | 230 ++++++++++++
...Record_WithAbstractParameters.verified.txt | 76 ++++
...d_WithGenericAbstractProperty.verified.txt | 84 +++++
...e=Record_WithIgnoredParameter.verified.txt | 0
...d_WithParametersAndProperties.verified.txt | 4 +-
...d_WithReferenceTypeParameters.verified.txt | 4 +-
...d_WithReferenceTypeProperties.verified.txt | 10 +-
...ame=Record_WithSchemaOverride.verified.txt | 0
...ecord_WithValueTypeParameters.verified.txt | 9 +-
...Options_testName=SimpleRecord.verified.txt | 33 ++
...Struct_WithAbstractProperties.verified.txt | 4 +-
...Struct_WithNullableValueTypes.verified.txt | 3 +-
...yMode_dictionaryKeyMode=Loose.verified.txt | 5 +-
...Mode_dictionaryKeyMode=Silent.verified.txt | 3 +-
...eyMode_dictionaryKeyMode=Skip.verified.txt | 5 +
...ode_dictionaryKeyMode=Strict.verified.txt} | 4 +-
...mHandling_enumHandling=String.verified.txt | 76 ++++
...g_enumHandling=UnderlyingType.verified.txt | 61 +++
...ify_Traversal_traversal=Bases.verified.txt | 8 +-
...rify_Traversal_traversal=Full.verified.txt | 8 +-
...raversal_traversal=Interfaces.verified.txt | 0
...raversal_traversal=SymbolOnly.verified.txt | 0
.../VerifyTests.cs | 65 ++--
.../SharpSchema.Test.Generator.csproj | 6 +-
79 files changed, 1769 insertions(+), 1185 deletions(-)
rename src/{SharpSchema.Generator/Accessibilities.cs => SharpSchema.Annotations/AccessibilityMode.cs} (83%)
rename src/{SharpSchema.Generator => SharpSchema.Annotations}/DictionaryKeyMode.cs (84%)
rename src/{SharpSchema.Generator/EnumHandling.cs => SharpSchema.Annotations/EnumMode.cs} (73%)
create mode 100644 src/SharpSchema.Annotations/GeneratorOptions.cs
create mode 100644 src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs
create mode 100644 src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs
create mode 100644 src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs
create mode 100644 src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs
rename src/{SharpSchema.Generator/Traversal.cs => SharpSchema.Annotations/TraversalMode.cs} (80%)
delete mode 100644 src/SharpSchema.Generator/GeneratorOptions.cs
delete mode 100644 src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs
create mode 100644 src/SharpSchema.Generator/LeafNodeSymbolVisitor.cs
create mode 100644 src/SharpSchema.Generator/LeafSyntaxVisitor.cs
create mode 100644 src/SharpSchema.Generator/Resolvers/CollectionResolver.cs
rename src/SharpSchema.Generator/{Utilities => Resolvers}/EnumSymbolVisitor.cs (78%)
rename src/SharpSchema.Generator/{RootDeclaredTypeSyntaxVisitor.cs => RootSyntaxVisitor.cs} (88%)
create mode 100644 src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs
create mode 100644 src/SharpSchema.Generator/Utilities/CollectionExtensions.cs
delete mode 100644 src/SharpSchema.Generator/Utilities/CollectionSymbolVisitor.cs
create mode 100644 src/SharpSchema.Generator/Utilities/CompilationExtensions.cs
delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt
delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt
delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt
delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/TestData.cs (71%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/TestDataFixture.cs (85%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt (96%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt (92%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt (91%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt (50%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt (92%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt (74%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt (98%)
create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt
create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt
create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt (91%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt (93%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt (92%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt (80%)
create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt (90%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt (98%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt (74%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt (87%)
create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt => RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt} (59%)
create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt
create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=Full.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt (100%)
rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.cs (68%)
diff --git a/.editorconfig b/.editorconfig
index 5c4c2f7..29d599f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -33,7 +33,7 @@ trim_trailing_whitespace = true
# MSBuild project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}]
-indent_size = 2
+indent_size = 4
# Xml config files
[*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}]
@@ -84,7 +84,7 @@ dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
# Constants are PascalCase
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
-dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
+dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.constants.applicable_kinds = field, local
dotnet_naming_symbols.constants.required_modifiers = const
@@ -94,7 +94,7 @@ dotnet_naming_style.constant_style.capitalization = pascal_case
# Static fields are camelCase
dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
-dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
+dotnet_naming_rule.static_fields_should_be_camel_case.style = camel_case_style
dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static
@@ -104,7 +104,7 @@ dotnet_naming_style.static_field_style.capitalization = camel_case
# Instance fields are camelCase
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
-dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
+dotnet_naming_rule.instance_fields_should_be_camel_case.style = camel_case_style
dotnet_naming_symbols.instance_fields.applicable_kinds = field
@@ -122,7 +122,7 @@ dotnet_naming_style.camel_case_style.capitalization = camel_case
# Local functions are PascalCase
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
-dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
@@ -131,7 +131,7 @@ dotnet_naming_style.local_function_style.capitalization = pascal_case
# By default, name items with PascalCase
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
-dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.all_members.applicable_kinds = *
@@ -209,6 +209,7 @@ dotnet_diagnostic.SA1130.severity = silent
# IDE1006: Naming Styles - StyleCop handles these for us
dotnet_diagnostic.IDE1006.severity = none
+dotnet_diagnostic.IDE0290.severity = silent
dotnet_diagnostic.DOC100.severity = silent
dotnet_diagnostic.DOC104.severity = warning
@@ -226,7 +227,7 @@ csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = file_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
-csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_primary_constructors = false:silent
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_expression_bodied_lambdas = when_on_single_line:silent
csharp_style_expression_bodied_local_functions = when_on_single_line:silent
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 93172cb..344e1ad 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,6 +6,7 @@
+
diff --git a/src/SharpSchema.Generator/Accessibilities.cs b/src/SharpSchema.Annotations/AccessibilityMode.cs
similarity index 83%
rename from src/SharpSchema.Generator/Accessibilities.cs
rename to src/SharpSchema.Annotations/AccessibilityMode.cs
index 78f45f9..2dadef7 100644
--- a/src/SharpSchema.Generator/Accessibilities.cs
+++ b/src/SharpSchema.Annotations/AccessibilityMode.cs
@@ -1,10 +1,17 @@
-namespace SharpSchema.Generator;
+using System;
+
+namespace SharpSchema.Annotations;
///
/// Specifies the allowed accessibilities.
///
[Flags]
-public enum Accessibilities
+#if SHARPSCHEMA_ASSEMBLY
+public
+#else
+internal
+#endif
+enum AccessibilityMode
{
///
/// Indicates public accessibility.
diff --git a/src/SharpSchema.Generator/DictionaryKeyMode.cs b/src/SharpSchema.Annotations/DictionaryKeyMode.cs
similarity index 84%
rename from src/SharpSchema.Generator/DictionaryKeyMode.cs
rename to src/SharpSchema.Annotations/DictionaryKeyMode.cs
index c1f482e..8ddb3df 100644
--- a/src/SharpSchema.Generator/DictionaryKeyMode.cs
+++ b/src/SharpSchema.Annotations/DictionaryKeyMode.cs
@@ -1,11 +1,16 @@
-using Json.Schema;
-
-namespace SharpSchema.Generator;
+
+namespace SharpSchema.Annotations;
///
/// Specifies the mode for dictionary keys.
///
-public enum DictionaryKeyMode
+#if SHARPSCHEMA_ASSEMBLY
+public
+#else
+internal
+#endif
+enum DictionaryKeyMode
+
{
///
/// Loose mode allows any type of dictionary key,
diff --git a/src/SharpSchema.Generator/EnumHandling.cs b/src/SharpSchema.Annotations/EnumMode.cs
similarity index 73%
rename from src/SharpSchema.Generator/EnumHandling.cs
rename to src/SharpSchema.Annotations/EnumMode.cs
index 8782410..f284800 100644
--- a/src/SharpSchema.Generator/EnumHandling.cs
+++ b/src/SharpSchema.Annotations/EnumMode.cs
@@ -1,9 +1,14 @@
-namespace SharpSchema.Generator;
+namespace SharpSchema.Annotations;
///
/// Specifies how enums are handled in the generated code.
///
-public enum EnumHandling
+#if SHARPSCHEMA_ASSEMBLY
+public
+#else
+internal
+#endif
+enum EnumMode
{
///
/// Enum values are represented as strings.
diff --git a/src/SharpSchema.Annotations/GeneratorOptions.cs b/src/SharpSchema.Annotations/GeneratorOptions.cs
new file mode 100644
index 0000000..8094507
--- /dev/null
+++ b/src/SharpSchema.Annotations/GeneratorOptions.cs
@@ -0,0 +1,29 @@
+using SharpSchema.Annotations;
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+namespace SharpSchema.Generator;
+#pragma warning restore IDE0130 // Namespace does not match folder structure
+
+///
+/// The generator options.
+///
+/// The accessibility mode.
+/// The traversal mode.
+/// The dictionary key mode.
+/// The enum mode.
+#if SHARPSCHEMA_ASSEMBLY
+public
+#else
+internal
+#endif
+record GeneratorOptions(
+ AccessibilityMode AccessibilityMode = AccessibilityMode.Public,
+ TraversalMode TraversalMode = TraversalMode.SymbolOnly,
+ DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose,
+ EnumMode EnumMode = EnumMode.String)
+{
+ ///
+ /// Gets the default generator options.
+ ///
+ public static GeneratorOptions Default { get; } = new GeneratorOptions();
+}
diff --git a/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs
new file mode 100644
index 0000000..05a2ea9
--- /dev/null
+++ b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+#nullable enable
+
+namespace SharpSchema.Annotations;
+
+///
+/// Overrides the default traversal option for a given type.
+///
+[AttributeUsage(SchemaAttribute.SupportedMembers)]
+#if SHARPSCHEMA_ASSEMBLY
+public class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute
+#else
+internal class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute
+#endif
+{
+ ///
+ /// Gets the accessibility option for the type.
+ ///
+ public AccessibilityMode Value { get; } = value;
+}
diff --git a/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs b/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs
new file mode 100644
index 0000000..443f823
--- /dev/null
+++ b/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+#nullable enable
+
+namespace SharpSchema.Annotations;
+
+///
+/// Overrides the default dictionary key mode for a given type.
+///
+[AttributeUsage(SchemaAttribute.SupportedMembers)]
+#if SHARPSCHEMA_ASSEMBLY
+public class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute
+#else
+internal class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute
+#endif
+{
+ ///
+ /// Gets the dictionary key mode.
+ ///
+ public DictionaryKeyMode Value { get; } = value;
+}
diff --git a/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs b/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs
new file mode 100644
index 0000000..bea6df3
--- /dev/null
+++ b/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+#nullable enable
+
+namespace SharpSchema.Annotations;
+
+///
+/// Overrides the default enum mode for a given type.
+///
+[AttributeUsage(AttributeTargets.Enum)]
+#if SHARPSCHEMA_ASSEMBLY
+public class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute
+#else
+internal class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute
+#endif
+{
+ ///
+ /// Gets the enum mode.
+ ///
+ public EnumMode Value { get; } = value;
+}
diff --git a/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs b/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs
new file mode 100644
index 0000000..7eeaa4d
--- /dev/null
+++ b/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+#nullable enable
+
+namespace SharpSchema.Annotations;
+
+///
+/// Overrides the default traversal option for a given type.
+///
+[AttributeUsage(SchemaAttribute.SupportedTypes)]
+#if SHARPSCHEMA_ASSEMBLY
+public class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute
+#else
+internal class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute
+#endif
+{
+ ///
+ /// Gets the traversal option for the type.
+ ///
+ public TraversalMode Value { get; } = value;
+}
diff --git a/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj b/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj
index 1a8ef26..e7f9f9c 100644
--- a/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj
+++ b/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj
@@ -8,4 +8,10 @@
Annotations library for the SharpSchema project.
SHARPSCHEMA_ASSEMBLY
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/src/SharpSchema.Generator/Traversal.cs b/src/SharpSchema.Annotations/TraversalMode.cs
similarity index 80%
rename from src/SharpSchema.Generator/Traversal.cs
rename to src/SharpSchema.Annotations/TraversalMode.cs
index d11bd1c..46c9c1c 100644
--- a/src/SharpSchema.Generator/Traversal.cs
+++ b/src/SharpSchema.Annotations/TraversalMode.cs
@@ -1,10 +1,17 @@
-namespace SharpSchema.Generator;
+using System;
+
+namespace SharpSchema.Annotations;
///
/// Specifies the traversal options for symbols.
///
[Flags]
-public enum Traversal
+#if SHARPSCHEMA_ASSEMBLY
+public
+#else
+internal
+#endif
+enum TraversalMode
{
///
/// Traverse only the symbol itself.
diff --git a/src/SharpSchema.Generator/GeneratorOptions.cs b/src/SharpSchema.Generator/GeneratorOptions.cs
deleted file mode 100644
index 8a4a249..0000000
--- a/src/SharpSchema.Generator/GeneratorOptions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace SharpSchema.Generator;
-
-///
-/// The options for the generator.
-///
-/// The accessibilities to consider.
-/// The traversal options.
-/// The mode for dictionary keys.
-/// The options for handling enums.
-public record GeneratorOptions(
- Accessibilities Accessibilities = Accessibilities.Public,
- Traversal Traversal = Traversal.SymbolOnly,
- DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose,
- EnumHandling EnumHandling = EnumHandling.String)
-{
- ///
- /// Gets the default generator options.
- ///
- public static GeneratorOptions Default { get; } = new GeneratorOptions();
-}
diff --git a/src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs b/src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs
deleted file mode 100644
index a12120f..0000000
--- a/src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs
+++ /dev/null
@@ -1,355 +0,0 @@
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis;
-using SharpSchema.Generator.Utilities;
-using SharpSchema.Generator.Model;
-using Json.Schema;
-using System.Text.Json.Nodes;
-
-namespace SharpSchema.Generator;
-
-using Builder = JsonSchemaBuilder;
-using Metadata = Model.Metadata;
-
-internal class LeafDeclaredTypeSyntaxVisitor : CSharpSyntaxVisitor
-{
- private readonly Compilation _compilation;
- private readonly GeneratorOptions _options;
- private readonly SemanticModelCache _semanticModelCache;
- private readonly Metadata.SymbolVisitor _metadataVisitor;
- private readonly CollectionSymbolVisitor _collectionVisitor;
- private readonly Dictionary _cachedTypeSchemas;
- private readonly Dictionary _cachedBaseSchemas;
-
- public LeafDeclaredTypeSyntaxVisitor(Compilation compilation, GeneratorOptions options)
- {
- _compilation = compilation;
- _options = options;
- _semanticModelCache = new(compilation);
- _metadataVisitor = new();
- _collectionVisitor = new(options, compilation, this);
- _cachedTypeSchemas = [];
- _cachedBaseSchemas = [];
- }
-
- internal GeneratorOptions Options => _options;
-
- public IReadOnlyDictionary? GetCachedSchemas()
- {
- if (_cachedTypeSchemas.Count == 0)
- return null;
-
- return _cachedTypeSchemas.ToDictionary(
- p => p.Key,
- p => p.Value.Build());
- }
-
- public override Builder? Visit(SyntaxNode? node)
- {
- if (node is null)
- return null;
-
- using var trace = Tracer.Enter($"[LEAF] {node.Kind()}");
- return base.Visit(node);
- }
-
- public override Builder? VisitQualifiedName(QualifiedNameSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.ToString());
-
- TypeInfo typeInfo = _semanticModelCache.GetSemanticModel(node).GetTypeInfo(node);
- if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for qualified name '{node}'.");
-
- return this.Visit(typeSymbol.FindBaseTypeDeclaration());
- }
-
- public override Builder? VisitClassDeclaration(ClassDeclarationSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Identifier.Text);
- return VisitTypeDeclaration(node);
- }
-
- public override Builder? VisitStructDeclaration(StructDeclarationSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Identifier.Text);
- return VisitTypeDeclaration(node);
- }
-
- public override Builder? VisitRecordDeclaration(RecordDeclarationSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Identifier.Text);
- return VisitTypeDeclaration(node);
- }
-
- public override Builder? VisitSimpleBaseType(SimpleBaseTypeSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Type.ToString());
- if (node.Type is not IdentifierNameSyntax nameSyntax)
- return CommonSchemas.UnsupportedObject($"The base type syntax is not supported for generation '{node.Type}'.");
-
- if (_semanticModelCache.GetSemanticModel(node).GetTypeInfo(node.Type).Type is not INamedTypeSymbol baseTypeSymbol)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for base type '{node.Type}'.");
-
- string cacheKey = baseTypeSymbol.GetDefCacheKey();
- if (!_cachedBaseSchemas.TryGetValue(cacheKey, out Builder? cachedSchema))
- {
- cachedSchema = baseTypeSymbol.FindTypeDeclaration()?.CreateTypeSchema(this);
- if (cachedSchema is not null)
- {
- _cachedBaseSchemas[cacheKey] = cachedSchema;
- }
- }
-
- return cachedSchema;
- }
-
- public override Builder? VisitEnumDeclaration(EnumDeclarationSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Identifier.Text);
-
- if (node.GetDeclaredSymbol(_semanticModelCache) is not INamedTypeSymbol enumSymbol)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for enum '{node.Identifier}'.");
-
- if (enumSymbol.GetOverrideSchema() is Builder overrideSchema)
- return overrideSchema;
-
- string cacheKey = enumSymbol.GetDefCacheKey();
- if (!_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema))
- {
-
- Metadata metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(enumSymbol));
-
- Builder builder = EnumSymbolVisitor.Instance.Visit(enumSymbol, _options) ??
- CommonSchemas.UnsupportedObject($"Failed to build schema for enum '{node.Identifier}'.");
-
- _cachedTypeSchemas[cacheKey] = builder.ApplyMetadata(metadata);
- }
-
- return CommonSchemas.DefRef(cacheKey);
- }
-
- public override Builder? VisitPropertyDeclaration(PropertyDeclarationSyntax node)
- {
- Throw.IfNullArgument(node);
-
- using var trace = Tracer.Enter(node.Identifier.Text);
-
- if (node.GetDeclaredSymbol(_semanticModelCache) is not IPropertySymbol propertySymbol)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for property '{node.Identifier}'.");
-
- if (!_options.ShouldProcess(propertySymbol))
- return null;
-
- if (propertySymbol.GetOverrideSchema() is Builder overrideSchema)
- return overrideSchema;
-
- Metadata metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(propertySymbol));
-
- return GetPropertySchema(node.Type, metadata, node.Initializer?.Value);
- }
-
- public override Builder? VisitParameter(ParameterSyntax node)
- {
- Throw.IfNullArgument(node);
-
- using var trace = Tracer.Enter(node.Identifier.Text);
-
- if (node.GetDeclaredSymbol(_semanticModelCache) is not IParameterSymbol parameterSymbol)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for parameter '{node.Identifier}'.");
-
- if (!_options.ShouldProcess(parameterSymbol))
- return null;
-
- if (parameterSymbol.GetOverrideSchema() is Builder overrideSchema)
- return overrideSchema;
-
- Metadata metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(parameterSymbol));
-
- TypeSyntax? typeSyntax = node.Type;
- if (typeSyntax is null)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for parameter type '{typeSyntax}'.");
-
- return GetPropertySchema(typeSyntax, metadata, node.Default?.Value);
- }
-
- public override Builder? VisitIdentifierName(IdentifierNameSyntax node)
- {
- Throw.IfNullArgument(node);
-
- using var trace = Tracer.Enter(node.Identifier.Text);
-
- TypeInfo typeInfo = _semanticModelCache.GetSemanticModel(node).GetTypeInfo(node);
- if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for identifier '{node.Identifier}'.");
-
- return this.Visit(typeSymbol.FindBaseTypeDeclaration())
- ?? CommonSchemas.UnsupportedObject($"Could not find declaration for identifier '{node.Identifier}'.");
- }
-
- public override Builder? VisitGenericName(GenericNameSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Identifier.Text);
-
- if (node.IsUnboundGenericName)
- return CommonSchemas.UnsupportedObject($"Failed to evaluate unbound generic type '{node.Identifier}'.");
-
- SemanticModel semanticModel = _semanticModelCache.GetSemanticModel(node);
-
- if (semanticModel.GetTypeInfo(node).Type is not INamedTypeSymbol boundTypeSymbol)
- return CommonSchemas.UnsupportedObject($"Node '{node.Identifier.Text}' does not produce a type symbol.");
-
- Builder? collectionBuilder = _collectionVisitor.Visit(boundTypeSymbol);
- if (collectionBuilder is not null)
- return collectionBuilder;
-
- return this.Visit(boundTypeSymbol.FindTypeDeclaration());
- }
-
- public override Builder? VisitNullableType(NullableTypeSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Kind().ToString());
-
- return CommonSchemas.Nullable(
- Throw.ForUnexpectedNull(
- node.ElementType.Accept(this)));
- }
-
- public override Builder? VisitPredefinedType(PredefinedTypeSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Keyword.Text);
-
- if (_semanticModelCache.GetSemanticModel(node).GetTypeInfo(node).Type is not INamedTypeSymbol typeSymbol)
- {
- return CommonSchemas.UnsupportedObject($"Failed to build schema for predefined type '{node}'.");
- }
-
- if (typeSymbol.IsJsonDefinedType(out Builder? valueTypeSchema))
- {
- return valueTypeSchema;
- }
-
- return CommonSchemas.UnsupportedObject($"Unexpected built-in type '{node.Keyword.Text}'.");
- }
-
- public override Builder? VisitArrayType(ArrayTypeSyntax node)
- {
- Throw.IfNullArgument(node);
- using var trace = Tracer.Enter(node.Kind().ToString());
-
- Builder? elementSchema = node.ElementType.Accept(this);
- if (elementSchema is null)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for array element type '{node.ElementType}'.");
-
- return CommonSchemas.ArrayOf(elementSchema);
- }
-
- private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node)
- {
- if (_semanticModelCache.GetSemanticModel(node).GetDeclaredSymbol(node) is not INamedTypeSymbol typeSymbol)
- return CommonSchemas.UnsupportedObject($"Failed building schema for {node.Identifier.ValueText}");
-
- string typeId = typeSymbol.GetDefCacheKey();
- if (_cachedTypeSchemas.TryGetValue(typeId, out _))
- return CommonSchemas.DefRef(typeId);
-
- if (typeSymbol.GetOverrideSchema() is Builder overrideSchema)
- return overrideSchema;
-
- // Handle abstract types
- if (typeSymbol.IsAbstract)
- {
- List oneOfBuilders = [];
- foreach (INamedTypeSymbol concrete in GetConcreteSubtypes(typeSymbol))
- {
- TypeDeclarationSyntax? decl = concrete.FindTypeDeclaration();
- if (decl is not null)
- {
- var subtypeBuilder = this.Visit(decl);
- if (subtypeBuilder is not null)
- oneOfBuilders.Add(subtypeBuilder);
- }
- }
-
- if (oneOfBuilders.Count > 0)
- {
- Builder abstractBuilder = CommonSchemas.Object; // Create a new def builder.
- abstractBuilder = abstractBuilder.OneOf([.. oneOfBuilders]);
- _cachedTypeSchemas[typeId] = abstractBuilder;
-
- return CommonSchemas.DefRef(typeId);
- }
- }
-
- // Regular concrete type.
- Builder builder = node.CreateTypeSchema(this);
-
- _cachedTypeSchemas[typeId] = builder;
- return CommonSchemas.DefRef(typeId);
- }
-
- // Helper to get all concrete subtypes of an abstract class.
- private IEnumerable GetConcreteSubtypes(INamedTypeSymbol abstractType)
- {
- using var trace = Tracer.Enter(abstractType.Name);
- foreach (SyntaxTree tree in _compilation.SyntaxTrees)
- {
- SemanticModel model = _compilation.GetSemanticModel(tree);
- foreach (var node in tree.GetRoot().DescendantNodes().OfType())
- {
- if (model.GetDeclaredSymbol(node) is not INamedTypeSymbol symbol)
- continue;
-
- if (!symbol.IsAbstract && symbol.InheritsFrom(abstractType))
- {
- trace.WriteLine($"Found concrete subtype: {symbol.Name}");
- yield return symbol;
- }
- }
- }
- }
-
- private Builder GetPropertySchema(TypeSyntax typeSyntax, Metadata metadata, ExpressionSyntax? defaultExpression)
- {
- SemanticModel semanticModel = _semanticModelCache.GetSemanticModel(typeSyntax);
- if (semanticModel.GetTypeInfo(typeSyntax).Type is not ITypeSymbol typeSymbol || !typeSymbol.IsValidForGeneration())
- return CommonSchemas.UnsupportedObject($"Failed to build schema for type '{typeSyntax}'.");
-
- Builder? propertyBuilder = null;
-
- // These kinds are never directly cached
- if (typeSyntax.Kind() is SyntaxKind.NullableType or SyntaxKind.ArrayType or SyntaxKind.PredefinedType)
- propertyBuilder = this.Visit(typeSyntax);
-
- string typeId = typeSymbol.GetDefCacheKey();
- if (propertyBuilder is null && _cachedTypeSchemas.TryGetValue(typeId, out _))
- propertyBuilder = CommonSchemas.DefRef(typeId);
-
- if (propertyBuilder is null && this.Visit(typeSyntax) is Builder builder)
- propertyBuilder = builder;
-
- if (propertyBuilder is null)
- return CommonSchemas.UnsupportedObject($"Failed to build schema for type '{typeSymbol.Name}'.");
-
- propertyBuilder.ApplyMetadata(metadata);
-
- JsonNode? defaultValue = null;
- if (defaultExpression is not null && semanticModel.GetConstantValue(defaultExpression) is { HasValue: true } constantValue)
- {
- defaultValue = JsonValue.Create(constantValue.Value);
- }
-
- return defaultValue is not null
- ? propertyBuilder.Default(defaultValue)
- : propertyBuilder;
- }
-}
diff --git a/src/SharpSchema.Generator/LeafNodeSymbolVisitor.cs b/src/SharpSchema.Generator/LeafNodeSymbolVisitor.cs
new file mode 100644
index 0000000..e69de29
diff --git a/src/SharpSchema.Generator/LeafSyntaxVisitor.cs b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs
new file mode 100644
index 0000000..95af9b8
--- /dev/null
+++ b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs
@@ -0,0 +1,349 @@
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using Json.Schema;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using SharpSchema.Annotations;
+using SharpSchema.Generator.Model;
+using SharpSchema.Generator.Resolvers;
+using SharpSchema.Generator.Utilities;
+
+namespace SharpSchema.Generator;
+
+using Builder = JsonSchemaBuilder;
+
+internal partial class LeafSyntaxVisitor : CSharpSyntaxVisitor
+{
+ private readonly Compilation _compilation;
+ private readonly GeneratorOptions _options;
+ private readonly SemanticModelCache _semanticModelCache;
+ private readonly MemberMeta.SymbolVisitor _metadataVisitor;
+ private readonly CollectionResolver _collectionResolver;
+ private readonly Dictionary _cachedTypeSchemas;
+ private readonly Dictionary _cachedAbstractSymbols;
+
+ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options)
+ {
+ _compilation = compilation;
+ _options = options;
+ _semanticModelCache = new(compilation);
+ _metadataVisitor = new();
+ _collectionResolver = new(compilation);
+ _cachedTypeSchemas = [];
+ _cachedAbstractSymbols = [];
+ }
+
+ internal GeneratorOptions Options => _options;
+
+ public IReadOnlyDictionary? GetCachedSchemas()
+ {
+ if (_cachedAbstractSymbols.Count > 0)
+ {
+ using var trace = Tracer.Enter("Building abstract type schemas.");
+ ImmutableArray namedTypes = [.. _compilation.GetAllNamedTypes(_semanticModelCache)];
+
+ foreach ((string key, INamedTypeSymbol abstractSymbol) in _cachedAbstractSymbols)
+ {
+ trace.WriteLine($"Building schema for abstract type '{abstractSymbol.Name}'.");
+
+ IEnumerable subTypes = namedTypes
+ .Where(t => t.Symbol.InheritsFrom(abstractSymbol));
+
+ List subSchemas = [];
+
+ foreach (NamedType subType in subTypes)
+ {
+ // Check if the sub-type is already cached
+ string cacheKey = subType.Symbol.GetDefCacheKey();
+ if (_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema))
+ {
+ trace.WriteLine($"Using cached schema for '{subType.Symbol.Name}'.");
+ subSchemas.Add(CommonSchemas.DefRef(cacheKey));
+ }
+ else
+ {
+ trace.WriteLine($"Building schema for '{subType.Symbol.Name}'.");
+ Builder? subSchema = this.CreateTypeSchema(subType.Symbol, subType.SyntaxNode);
+ if (subSchema is not null)
+ {
+ _cachedTypeSchemas[cacheKey] = subSchema;
+ subSchemas.Add(CommonSchemas.DefRef(cacheKey));
+ }
+ }
+ }
+
+ if (subSchemas.Count > 0)
+ {
+ trace.WriteLine($"Found {subSchemas.Count} sub-types for '{abstractSymbol.Name}'.");
+ Builder abstractSchema = CommonSchemas.Object.OneOf(subSchemas);
+ _cachedTypeSchemas[key] = abstractSchema;
+ }
+ else
+ trace.WriteLine($"No sub-types found for '{abstractSymbol.Name}'.");
+ }
+ }
+
+ if (_cachedTypeSchemas.Count == 0)
+ return null;
+
+ return _cachedTypeSchemas.ToDictionary(
+ p => p.Key,
+ p => p.Value.Build());
+ }
+
+ [ExcludeFromCodeCoverage]
+ public override Builder? DefaultVisit(SyntaxNode node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope scope = Tracer.Enter($"{node.Kind()} {node.GetLocation().GetLineSpan().StartLinePosition.Line}");
+ return null;
+ }
+
+ public override Builder? VisitQualifiedName(QualifiedNameSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Right.Identifier.Text);
+ return this.Visit(node.Right);
+ }
+
+ public override Builder? VisitClassDeclaration(ClassDeclarationSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text);
+ return this.VisitTypeDeclaration(node, trace);
+ }
+
+ public override Builder? VisitStructDeclaration(StructDeclarationSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text);
+ return this.VisitTypeDeclaration(node, trace);
+ }
+
+ public override Builder? VisitRecordDeclaration(RecordDeclarationSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text);
+ return this.VisitTypeDeclaration(node, trace);
+ }
+
+ public override Builder? VisitEnumDeclaration(EnumDeclarationSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text);
+
+ if (node.GetDeclaredSymbol(_semanticModelCache) is not INamedTypeSymbol enumSymbol)
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for enum '{node.Identifier}'.");
+
+ if (enumSymbol.GetOverrideSchema() is Builder overrideSchema)
+ return overrideSchema;
+
+ string cacheKey = enumSymbol.GetDefCacheKey();
+ if (!_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema))
+ {
+ MemberMeta metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(enumSymbol));
+
+ Builder builder = EnumSymbolVisitor.Instance.Visit(enumSymbol, _options) ??
+ CommonSchemas.UnsupportedObject($"Failed to build schema for enum '{node.Identifier}'.");
+
+ _cachedTypeSchemas[cacheKey] = builder.ApplyMemberMeta(metadata);
+ }
+
+ return CommonSchemas.DefRef(cacheKey);
+ }
+
+ public override Builder? VisitIdentifierName(IdentifierNameSyntax node)
+ {
+ Throw.IfNullArgument(node);
+
+ using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text);
+
+ TypeInfo typeInfo = node.GetTypeInfo(_semanticModelCache);
+ if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol)
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for identifier '{node.Identifier}'.");
+
+ return this.Visit(typeSymbol.FindDeclaringSyntax())
+ ?? CommonSchemas.UnsupportedObject($"Could not find declaration for identifier '{node.Identifier}'.");
+ }
+
+ public override Builder? VisitGenericName(GenericNameSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text);
+
+ if (node.IsUnboundGenericName)
+ return CommonSchemas.UnsupportedObject($"Failed to evaluate unbound generic type '{node.Identifier}'.");
+
+ SemanticModel semanticModel = _semanticModelCache.GetSemanticModel(node);
+
+ if (semanticModel.GetTypeInfo(node).Type is not INamedTypeSymbol boundTypeSymbol)
+ return CommonSchemas.UnsupportedObject($"Node '{node.Identifier.Text}' does not produce a type symbol.");
+
+ var (kind, keyType, elementSymbol) = _collectionResolver.Resolve(boundTypeSymbol);
+ if (kind is CollectionKind.Dictionary)
+ {
+ if (!elementSymbol.IsJsonDefinedType(out Builder? elementSchema))
+ {
+ if (elementSymbol.FindDeclaringSyntax() is BaseTypeDeclarationSyntax elx)
+ elementSchema = elx.Accept(this);
+ }
+
+ if (elementSchema is null)
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for dictionary element type '{elementSymbol}'.");
+
+ Builder builder = CommonSchemas.Object;
+
+ if (keyType is not SchemaValueType.String)
+ {
+ switch (_options.DictionaryKeyMode)
+ {
+ case DictionaryKeyMode.Skip:
+ return null;
+ case DictionaryKeyMode.Strict:
+ return CommonSchemas.UnsupportedObject($"Key type '{keyType}' is not supported.");
+ case DictionaryKeyMode.Loose:
+ builder = builder.Comment($"Key type '{keyType}' must be convertible to string");
+ break;
+ case DictionaryKeyMode.Silent:
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected dictionary key mode '{_options.DictionaryKeyMode}'.");
+ }
+ }
+
+ return builder.AdditionalProperties(elementSchema);
+ }
+
+ if (kind is CollectionKind.Array)
+ {
+ if (!elementSymbol.IsJsonDefinedType(out Builder? elementSchema))
+ {
+ if (elementSymbol.FindDeclaringSyntax() is BaseTypeDeclarationSyntax elx)
+ elementSchema = elx.Accept(this);
+ }
+
+ if (elementSchema is null)
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for array element type '{elementSymbol}'.");
+
+ return CommonSchemas.ArrayOf(elementSchema);
+ }
+
+ Builder? boundTypeBuilder = this.Visit(boundTypeSymbol.FindDeclaringSyntax());
+ if (boundTypeBuilder is null)
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for generic type '{node.Identifier}'.");
+
+ // Add to the oneOf for the unbound generic type
+ INamedTypeSymbol unboundGeneric = boundTypeSymbol.ConstructUnboundGenericType();
+ string cacheKey = unboundGeneric.GetDefCacheKey();
+ if (_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema))
+ {
+ IReadOnlyList? currentOneOf = cachedSchema.Get()?.Schemas;
+ cachedSchema = currentOneOf is not null
+ ? cachedSchema.OneOf(currentOneOf.Append(boundTypeBuilder))
+ : CommonSchemas.Object.OneOf(cachedSchema, boundTypeBuilder);
+ }
+
+ return boundTypeBuilder;
+ }
+
+ public override Builder? VisitNullableType(NullableTypeSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Kind().ToString());
+
+ Builder? elementSchema = this.Visit(node.ElementType);
+ return elementSchema is null
+ ? new Builder().Const(null)
+ : CommonSchemas.Nullable(
+ Throw.ForUnexpectedNull(
+ node.ElementType.Accept(this)));
+ }
+
+ public override Builder? VisitPredefinedType(PredefinedTypeSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Keyword.Text);
+
+ if (_semanticModelCache.GetSemanticModel(node).GetTypeInfo(node).Type is not INamedTypeSymbol typeSymbol)
+ {
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for predefined type '{node}'.");
+ }
+
+ if (typeSymbol.IsJsonDefinedType(out Builder? valueTypeSchema))
+ {
+ return valueTypeSchema;
+ }
+
+ return CommonSchemas.UnsupportedObject($"Unexpected built-in type '{node.Keyword.Text}'.");
+ }
+
+ public override Builder? VisitArrayType(ArrayTypeSyntax node)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Kind().ToString());
+
+ Builder? elementSchema = node.ElementType.Accept(this);
+ if (elementSchema is null)
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for array element type '{node.ElementType}'.");
+
+ return CommonSchemas.ArrayOf(elementSchema);
+ }
+
+ public Builder CreateTypeSchema(TypeDeclarationSyntax node)
+ {
+ Throw.IfNullArgument(node);
+
+ if (node.GetDeclaredSymbol(_semanticModelCache) is not INamedTypeSymbol typeSymbol)
+ return CommonSchemas.UnsupportedObject($"Failed to build schema for type '{node.Identifier}'.");
+
+ return this.CreateTypeSchema(typeSymbol, node);
+ }
+
+ private Builder CreateTypeSchema(INamedTypeSymbol symbol, TypeDeclarationSyntax node, TraversalMode? traversalMode = null)
+ {
+ Throw.IfNullArgument(node);
+ using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text);
+
+ return symbol.Accept(
+ new TypeSchemaSymbolVisitor(
+ this,
+ _semanticModelCache,
+ _options),
+ node)
+ ?? CommonSchemas.UnsupportedObject(symbol.Name);
+ }
+
+ private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node, Tracer.TraceScope trace)
+ {
+ if (_semanticModelCache.GetSemanticModel(node).GetDeclaredSymbol(node) is not INamedTypeSymbol typeSymbol)
+ return CommonSchemas.UnsupportedObject($"Failed building schema for {node.Identifier.ValueText}");
+
+ string typeId = typeSymbol.GetDefCacheKey();
+ if (_cachedTypeSchemas.TryGetValue(typeId, out _))
+ return CommonSchemas.DefRef(typeId);
+
+ if (typeSymbol.GetOverrideSchema() is Builder overrideSchema)
+ return overrideSchema;
+
+ // Handle abstract types
+ if (typeSymbol.IsAbstract)
+ {
+ trace.WriteLine($"Found abstract type '{typeSymbol.Name}'.");
+ if (!_cachedAbstractSymbols.TryGetValue(typeId, out _))
+ {
+ _cachedAbstractSymbols.Add(typeId, typeSymbol);
+ }
+
+ return CommonSchemas.DefRef(typeId);
+ }
+
+ AttributeHandler schemaTraversal = typeSymbol.GetAttributeHandler();
+
+ // Regular concrete type.
+ Builder builder = CreateTypeSchema(typeSymbol, node, schemaTraversal.Get(0) ?? _options.TraversalMode);
+
+ _cachedTypeSchemas[typeId] = builder;
+ return CommonSchemas.DefRef(typeId);
+ }
+}
diff --git a/src/SharpSchema.Generator/Model/Metadata.cs b/src/SharpSchema.Generator/Model/Metadata.cs
index 0de76ce..a797a55 100644
--- a/src/SharpSchema.Generator/Model/Metadata.cs
+++ b/src/SharpSchema.Generator/Model/Metadata.cs
@@ -14,7 +14,7 @@ namespace SharpSchema.Generator.Model;
/// Examples for the schema member.
/// Additional comments for the schema member.
/// Indicates if the schema member is deprecated.
-public record Metadata(
+public record MemberMeta(
string Title,
string? Description,
List? Examples,
@@ -31,7 +31,7 @@ public override string ToString()
///
/// A visitor that extracts member metadata from symbols.
///
- internal class SymbolVisitor : SymbolVisitor
+ internal class SymbolVisitor : SymbolVisitor
{
private const string JsonSchemaTag = "jsonschema";
private const string TitleElement = "title";
@@ -40,33 +40,35 @@ internal class SymbolVisitor : SymbolVisitor
private const string ExampleElement = "example";
private const string DeprecatedElement = "deprecated";
+ public static SymbolVisitor Default { get; } = new();
+
///
- /// Visits a named type symbol and extracts .
+ /// Visits a named type symbol and extracts .
///
/// The named type symbol to visit.
- /// The extracted .
- public override Metadata VisitNamedType(INamedTypeSymbol symbol) => CreateMetadata(symbol);
+ /// The extracted .
+ public override MemberMeta VisitNamedType(INamedTypeSymbol symbol) => CreateMetadata(symbol);
///
- /// Visits a property symbol and extracts .
+ /// Visits a property symbol and extracts .
///
/// The property symbol to visit.
- /// The extracted .
- public override Metadata VisitProperty(IPropertySymbol symbol) => CreateMetadata(symbol);
+ /// The extracted .
+ public override MemberMeta VisitProperty(IPropertySymbol symbol) => CreateMetadata(symbol);
///
- /// Visits a parameter symbol and extracts .
+ /// Visits a parameter symbol and extracts .
///
/// The parameter symbol to visit.
- /// The extracted .
- public override Metadata VisitParameter(IParameterSymbol symbol) => CreateMetadata(symbol);
+ /// The extracted .
+ public override MemberMeta VisitParameter(IParameterSymbol symbol) => CreateMetadata(symbol);
///
- /// Creates a instance from the given symbol.
+ /// Creates a instance from the given symbol.
///
/// The symbol to extract metadata from.
- /// The created instance.
- private static Metadata CreateMetadata(ISymbol symbol)
+ /// The created instance.
+ private static MemberMeta CreateMetadata(ISymbol symbol)
{
using var trace = Tracer.Enter(symbol.Name);
@@ -129,7 +131,7 @@ private static Metadata CreateMetadata(ISymbol symbol)
deprecated = data.GetNamedArgument(nameof(SchemaMetaAttribute.Deprecated)) || deprecated;
}
- return new Metadata(title, description, examples, comment, deprecated);
+ return new MemberMeta(title, description, examples, comment, deprecated);
}
}
}
diff --git a/src/SharpSchema.Generator/Resolvers/CollectionResolver.cs b/src/SharpSchema.Generator/Resolvers/CollectionResolver.cs
new file mode 100644
index 0000000..91ad436
--- /dev/null
+++ b/src/SharpSchema.Generator/Resolvers/CollectionResolver.cs
@@ -0,0 +1,72 @@
+using Json.Schema;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using SharpSchema.Generator.Utilities;
+
+namespace SharpSchema.Generator.Resolvers;
+
+internal enum CollectionKind
+{
+ None,
+ Array,
+ Dictionary
+}
+
+internal class CollectionResolver
+{
+ internal readonly record struct Result(CollectionKind Kind, SchemaValueType KeyType, ITypeSymbol ElementSymbol);
+
+ private const string IEnumerableMetadataName = "System.Collections.Generic.IEnumerable`1";
+ private const string IDictionaryMetadataName = "System.Collections.Generic.IDictionary`2";
+ private const string IReadOnlyDictionaryMetadataName = "System.Collections.Generic.IReadOnlyDictionary`2";
+
+ private readonly INamedTypeSymbol _dictionaryOfKVSymbol;
+ private readonly INamedTypeSymbol _readOnlyDictionaryOfKVSymbol;
+ private readonly INamedTypeSymbol _enumerableOfTSymbol;
+
+ public CollectionResolver(Compilation compilation)
+ {
+ _enumerableOfTSymbol = compilation.GetTypeByMetadataName(IEnumerableMetadataName)
+ ?? throw new InvalidOperationException($"Could not find symbol for '{IEnumerableMetadataName}'.");
+
+ _readOnlyDictionaryOfKVSymbol = compilation.GetTypeByMetadataName(IReadOnlyDictionaryMetadataName)
+ ?? throw new InvalidOperationException($"Could not find symbol for '{IReadOnlyDictionaryMetadataName}'.");
+
+ _dictionaryOfKVSymbol = compilation.GetTypeByMetadataName(IDictionaryMetadataName) ??
+ throw new InvalidOperationException($"Could not find symbol for '{IDictionaryMetadataName}'.");
+ }
+
+ public Result Resolve(INamedTypeSymbol boundTypeSymbol)
+ {
+ using var trace = Tracer.Enter(boundTypeSymbol.Name);
+
+ // Try dictionary resolution
+ if (boundTypeSymbol.ImplementsGenericInterface(_dictionaryOfKVSymbol, _readOnlyDictionaryOfKVSymbol)
+ is INamedTypeSymbol boundDictionarySymbol)
+ {
+ trace.WriteLine("Dictionary type found.");
+
+ ITypeSymbol keyTypeSymbol = boundDictionarySymbol.TypeArguments.First();
+ ITypeSymbol valueTypeSymbol = boundDictionarySymbol.TypeArguments.Last();
+
+ return new(
+ CollectionKind.Dictionary,
+ keyTypeSymbol.GetSchemaValueType(),
+ valueTypeSymbol);
+ }
+
+ // Try enumerable resolution
+ if (boundTypeSymbol.ImplementsGenericInterface(_enumerableOfTSymbol) is INamedTypeSymbol boundEnumerableSymbol)
+ {
+ trace.WriteLine("Enumerable type found.");
+ ITypeSymbol elementTypeSymbol = boundEnumerableSymbol.TypeArguments.First();
+ return new(
+ CollectionKind.Array,
+ SchemaValueType.Null,
+ elementTypeSymbol);
+ }
+
+ trace.WriteLine("Not a recognized collection.");
+ return new(CollectionKind.None, SchemaValueType.Null, null!);
+ }
+}
diff --git a/src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs
similarity index 78%
rename from src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs
rename to src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs
index a518e0d..a780793 100644
--- a/src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs
+++ b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs
@@ -3,8 +3,9 @@
using Microsoft.CodeAnalysis;
using SharpSchema.Annotations;
using SharpSchema.Generator.Model;
+using SharpSchema.Generator.Utilities;
-namespace SharpSchema.Generator.Utilities;
+namespace SharpSchema.Generator.Resolvers;
using Builder = JsonSchemaBuilder;
@@ -23,19 +24,19 @@ private EnumSymbolVisitor() { }
if (symbol.TypeKind != TypeKind.Enum)
return DefaultResult;
- trace.WriteLine($"{options.EnumHandling}");
+ trace.WriteLine($"{options.EnumMode}");
- if (options.EnumHandling == EnumHandling.String)
+ if (options.EnumMode == EnumMode.String)
{
var names = symbol.GetMembers()
.OfType()
- .Select(fieldSymbol => (fieldSymbol.GetAttributeHandler()[0] as string)
+ .Select(fieldSymbol => fieldSymbol.GetAttributeHandler()[0] as string
?? fieldSymbol.Name.Camelize())
.ToList();
return CommonSchemas.String.Enum(names);
}
- else if (options.EnumHandling == EnumHandling.UnderlyingType)
+ else if (options.EnumMode == EnumMode.UnderlyingType)
{
trace.WriteLine("Underlying type enum handling.");
diff --git a/src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs b/src/SharpSchema.Generator/RootSyntaxVisitor.cs
similarity index 88%
rename from src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs
rename to src/SharpSchema.Generator/RootSyntaxVisitor.cs
index 0cf88b6..ec9b4f9 100644
--- a/src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs
+++ b/src/SharpSchema.Generator/RootSyntaxVisitor.cs
@@ -11,16 +11,16 @@ namespace SharpSchema.Generator;
///
/// Visits C# syntax nodes to generate JSON schema builders.
///
-public class RootDeclaredTypeSyntaxVisitor : CSharpSyntaxVisitor
+public class RootSyntaxVisitor : CSharpSyntaxVisitor
{
- private readonly LeafDeclaredTypeSyntaxVisitor _cachingVisitor;
+ private readonly LeafSyntaxVisitor _cachingVisitor;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The compilation context.
/// The generator options.
- public RootDeclaredTypeSyntaxVisitor(Compilation compilation, GeneratorOptions options)
+ public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options)
{
_cachingVisitor = new(compilation, options);
}
@@ -94,7 +94,7 @@ public RootDeclaredTypeSyntaxVisitor(Compilation compilation, GeneratorOptions o
/// A JSON schema builder or null.
private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node)
{
- Builder builder = node.CreateTypeSchema(_cachingVisitor);
+ Builder builder = _cachingVisitor.CreateTypeSchema(node);
if (_cachingVisitor.GetCachedSchemas() is IReadOnlyDictionary defs)
{
diff --git a/src/SharpSchema.Generator/SharpSchema.Generator.csproj b/src/SharpSchema.Generator/SharpSchema.Generator.csproj
index cc4699a..a5c9ace 100644
--- a/src/SharpSchema.Generator/SharpSchema.Generator.csproj
+++ b/src/SharpSchema.Generator/SharpSchema.Generator.csproj
@@ -21,10 +21,6 @@
-
-
-
-
diff --git a/src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs b/src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs
new file mode 100644
index 0000000..1074e78
--- /dev/null
+++ b/src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs
@@ -0,0 +1,193 @@
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis;
+using SharpSchema.Generator.Utilities;
+using SharpSchema.Generator.Model;
+using Json.Schema;
+using Humanizer;
+using SharpSchema.Annotations;
+using System.Text.Json.Nodes;
+
+namespace SharpSchema.Generator;
+
+using Builder = JsonSchemaBuilder;
+
+internal class TypeSchemaSymbolVisitor : SymbolVisitor
+{
+ private readonly CSharpSyntaxVisitor _syntaxVisitor;
+ private readonly SemanticModelCache _semanticModelCache;
+ private readonly GeneratorOptions _options;
+
+ public TypeSchemaSymbolVisitor(
+ CSharpSyntaxVisitor visitor,
+ SemanticModelCache semanticModelCache,
+ GeneratorOptions options)
+ {
+ _syntaxVisitor = visitor;
+ _semanticModelCache = semanticModelCache;
+ _options = options;
+ }
+
+ protected override Builder? DefaultResult { get; }
+
+ public override Builder? VisitNamedType(INamedTypeSymbol symbol, BaseTypeDeclarationSyntax argument)
+ {
+ using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}");
+
+ Dictionary properties = new(StringComparer.OrdinalIgnoreCase);
+ HashSet _requiredProperties = new(StringComparer.OrdinalIgnoreCase);
+
+ if (symbol.IsRecord)
+ {
+ IMethodSymbol primaryCtor = symbol.Constructors.First();
+ primaryCtor.Parameters.ForEach(param =>
+ {
+ Builder? typeBuilder = param.Accept(this, argument);
+ if (typeBuilder is null)
+ return;
+
+ string propertyName = param.Name.Camelize();
+ properties.Add(propertyName, typeBuilder);
+
+ bool isNullable = param.NullableAnnotation switch
+ {
+ NullableAnnotation.NotAnnotated => false,
+ NullableAnnotation.Annotated => true,
+ NullableAnnotation.None => false, // TODO: Make Configurable
+ _ => throw new NotSupportedException()
+ };
+
+ bool isRequired = !isNullable;
+ if (param.HasExplicitDefaultValue)
+ isRequired = false;
+
+ if (EvaluateSchemaRequired(param, isRequired))
+ _requiredProperties.Add(propertyName);
+ });
+ }
+
+ symbol.GetMembers().OfType().ForEach(prop =>
+ {
+ Builder? valueBuilder = prop.Accept(this, argument);
+ if (valueBuilder is null)
+ return;
+
+ string propertyName = prop.Name.Camelize();
+ properties.Add(propertyName, valueBuilder);
+
+ bool isNullable = prop.NullableAnnotation switch
+ {
+ NullableAnnotation.NotAnnotated => false,
+ NullableAnnotation.Annotated => true,
+ NullableAnnotation.None => false, // TODO: Make Configurable
+ _ => throw new NotSupportedException()
+ };
+
+ bool isRequired = !isNullable;
+ if (EvaluateSchemaRequired(prop, isRequired))
+ _requiredProperties.Add(propertyName);
+ });
+
+ Builder builder = CommonSchemas.Object;
+ if (properties.Count > 0)
+ builder = builder.Properties(properties);
+
+ if (_requiredProperties.Count > 0)
+ builder = builder.Required(_requiredProperties);
+
+ if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta)
+ builder = builder.ApplyMemberMeta(meta);
+
+ return builder;
+
+ static bool EvaluateSchemaRequired(ISymbol prop, bool isRequired)
+ {
+ AttributeHandler schemaRequired = prop.GetAttributeHandler();
+ if (schemaRequired[0] is bool overrideRequired)
+ return overrideRequired;
+
+ return isRequired;
+ }
+ }
+
+ public override Builder? VisitProperty(IPropertySymbol symbol, BaseTypeDeclarationSyntax argument)
+ {
+ using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}");
+
+ if (!_options.ShouldProcess(symbol) || !symbol.IsValidForGeneration())
+ return null;
+
+ // Excludes generated properties.
+ if (symbol.FindDeclaringSyntax() is PropertyDeclarationSyntax pdx)
+ {
+ Builder? typeBuilder = pdx.Type.Accept(_syntaxVisitor);
+ if (typeBuilder is null)
+ return null;
+
+ if (pdx.ExpressionBody is ArrowExpressionClauseSyntax aec
+ && GetConstantValue(aec.Expression) is JsonNode constantValue)
+ {
+ typeBuilder = CommonSchemas.Object.Const(constantValue);
+ }
+ else if (ExtractDefaultValue(pdx) is JsonNode defaultValue)
+ {
+ typeBuilder = typeBuilder.Default(defaultValue);
+ }
+
+ if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta)
+ typeBuilder = typeBuilder.ApplyMemberMeta(meta);
+
+ return typeBuilder;
+ }
+
+ return null;
+
+ // -- Local functions --
+
+ JsonNode? ExtractDefaultValue(PropertyDeclarationSyntax propertyDeclaration)
+ {
+ return propertyDeclaration.Initializer is EqualsValueClauseSyntax evc
+ ? GetConstantValue(evc.Value)
+ : null;
+ }
+
+ JsonNode? GetConstantValue(SyntaxNode node)
+ {
+ SemanticModel sm = _semanticModelCache.GetSemanticModel(node);
+ return sm.GetConstantValue(node) is Optional