From cd866af765f04d7c88823b3850a874935b5f23e4 Mon Sep 17 00:00:00 2001 From: Beakona <branislav@guarami.com> Date: Fri, 1 Dec 2023 14:49:02 +0100 Subject: [PATCH] initial implementation.. --- .editorconfig | 247 +++++++++++ .github/workflows/codeql-analysis.yml | 68 +++ .github/workflows/dotnet-core.yml | 31 ++ .github/workflows/nuget.yml | 72 ++++ AutoAs.sln | 37 ++ AutoAsSample/AutoAsSample.csproj | 15 + AutoAsSample/Program.cs | 56 +++ .../BeaKona.AutoAsAttributes.csproj | 18 + .../GenerateAutoAsAttribute.cs | 12 + .../AutoAsResource.Designer.cs | 171 ++++++++ BeaKona.AutoAsGenerator/AutoAsResource.resx | 156 +++++++ .../BeaKona.AutoAsGenerator.csproj | 52 +++ .../CSharpCodeTextWriter.cs | 388 ++++++++++++++++++ .../GenerateAutoAsSourceGenerator.cs | 320 +++++++++++++++ BeaKona.AutoAsGenerator/Globals.cs | 4 + BeaKona.AutoAsGenerator/Helpers.cs | 26 ++ BeaKona.AutoAsGenerator/ICodeTextWriter.cs | 18 + .../IEnumerableExtensions.cs | 8 + .../INamedTypeSymbolExtensions.cs | 23 ++ .../INamespaceSymbolExtensions.cs | 16 + BeaKona.AutoAsGenerator/ScopeInfo.cs | 95 +++++ BeaKona.AutoAsGenerator/SemanticFacts.cs | 88 ++++ BeaKona.AutoAsGenerator/SourceBuilder.cs | 226 ++++++++++ .../SourceBuilderOptions.cs | 54 +++ 24 files changed, 2201 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/dotnet-core.yml create mode 100644 .github/workflows/nuget.yml create mode 100644 AutoAs.sln create mode 100644 AutoAsSample/AutoAsSample.csproj create mode 100644 AutoAsSample/Program.cs create mode 100644 BeaKona.AutoAsAttributes/BeaKona.AutoAsAttributes.csproj create mode 100644 BeaKona.AutoAsAttributes/GenerateAutoAsAttribute.cs create mode 100644 BeaKona.AutoAsGenerator/AutoAsResource.Designer.cs create mode 100644 BeaKona.AutoAsGenerator/AutoAsResource.resx create mode 100644 BeaKona.AutoAsGenerator/BeaKona.AutoAsGenerator.csproj create mode 100644 BeaKona.AutoAsGenerator/CSharpCodeTextWriter.cs create mode 100644 BeaKona.AutoAsGenerator/GenerateAutoAsSourceGenerator.cs create mode 100644 BeaKona.AutoAsGenerator/Globals.cs create mode 100644 BeaKona.AutoAsGenerator/Helpers.cs create mode 100644 BeaKona.AutoAsGenerator/ICodeTextWriter.cs create mode 100644 BeaKona.AutoAsGenerator/IEnumerableExtensions.cs create mode 100644 BeaKona.AutoAsGenerator/INamedTypeSymbolExtensions.cs create mode 100644 BeaKona.AutoAsGenerator/INamespaceSymbolExtensions.cs create mode 100644 BeaKona.AutoAsGenerator/ScopeInfo.cs create mode 100644 BeaKona.AutoAsGenerator/SemanticFacts.cs create mode 100644 BeaKona.AutoAsGenerator/SourceBuilder.cs create mode 100644 BeaKona.AutoAsGenerator/SourceBuilderOptions.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ed8729d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,247 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case +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_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_collection_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..4af58cd --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# ******** NOTE ******** + +name: "CodeQL" + +on: + push: + branches: [ main, master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, master ] + schedule: + - cron: '22 10 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml new file mode 100644 index 0000000..6abed5e --- /dev/null +++ b/.github/workflows/dotnet-core.yml @@ -0,0 +1,31 @@ +name: .NET Core + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_VERSION: 8.0.x + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + run: dotnet test --no-restore --verbosity normal diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml new file mode 100644 index 0000000..19f65b2 --- /dev/null +++ b/.github/workflows/nuget.yml @@ -0,0 +1,72 @@ +name: NuGet + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + release: + types: + - published + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_VERSION: 8.0.x + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + +jobs: + build: + if: github.event_name == 'release' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + run: dotnet test --no-restore --verbosity normal + + pack: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Pack + run: | + arrTag=(${GITHUB_REF//\// }) + VERSION="${arrTag[2]}" + echo Version: $VERSION + VERSION="${VERSION//v}" + dotnet pack -c Release -p:PackageVersion=$VERSION BeaKona.AutoAsGenerator + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: nupkg + path: | + ./**/bin/Release/*.nupkg + + push: + needs: pack + runs-on: ubuntu-latest + steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Download Artifact + uses: actions/download-artifact@v1 + with: + name: nupkg + - name: Push to nuget.org + run: dotnet nuget push ./nupkg/**/*.* --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NugetApiKey }} diff --git a/AutoAs.sln b/AutoAs.sln new file mode 100644 index 0000000..1c25518 --- /dev/null +++ b/AutoAs.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34321.82 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeaKona.AutoAsGenerator", "BeaKona.AutoAsGenerator\BeaKona.AutoAsGenerator.csproj", "{26A63262-E5F2-4652-8911-1AF2231F65F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeaKona.AutoAsAttributes", "BeaKona.AutoAsAttributes\BeaKona.AutoAsAttributes.csproj", "{0579C9D8-A6A4-42C4-981E-0BCC853A04E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoAsSample", "AutoAsSample\AutoAsSample.csproj", "{DBA68BE8-4627-4CFF-96FE-3688253861B4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {26A63262-E5F2-4652-8911-1AF2231F65F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26A63262-E5F2-4652-8911-1AF2231F65F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26A63262-E5F2-4652-8911-1AF2231F65F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26A63262-E5F2-4652-8911-1AF2231F65F7}.Release|Any CPU.Build.0 = Release|Any CPU + {0579C9D8-A6A4-42C4-981E-0BCC853A04E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0579C9D8-A6A4-42C4-981E-0BCC853A04E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0579C9D8-A6A4-42C4-981E-0BCC853A04E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0579C9D8-A6A4-42C4-981E-0BCC853A04E9}.Release|Any CPU.Build.0 = Release|Any CPU + {DBA68BE8-4627-4CFF-96FE-3688253861B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBA68BE8-4627-4CFF-96FE-3688253861B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBA68BE8-4627-4CFF-96FE-3688253861B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBA68BE8-4627-4CFF-96FE-3688253861B4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1DF42858-9E11-42F2-9681-9F55C549F816} + EndGlobalSection +EndGlobal diff --git a/AutoAsSample/AutoAsSample.csproj b/AutoAsSample/AutoAsSample.csproj new file mode 100644 index 0000000..10c6d8d --- /dev/null +++ b/AutoAsSample/AutoAsSample.csproj @@ -0,0 +1,15 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <Analyzer Include="..\BeaKona.AutoAsGenerator\bin\Debug\netstandard2.0\BeaKona.AutoAsGenerator.dll" /> + <ProjectReference Include="..\BeaKona.AutoAsAttributes\BeaKona.AutoAsAttributes.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" /> + </ItemGroup> + +</Project> diff --git a/AutoAsSample/Program.cs b/AutoAsSample/Program.cs new file mode 100644 index 0000000..e0e5e3b --- /dev/null +++ b/AutoAsSample/Program.cs @@ -0,0 +1,56 @@ +using BeaKona; +using System.Collections; + +namespace AutoAsSample; + +internal partial class Program +{ + static void Main() + { + //System.Diagnostics.Debug.WriteLine(BeaKona.Output.Debug_TestClass_1.Info); + + TestClass<int> t = new TestClass<int>(); + var x = t.AsMy1(); + } +} + +public interface IMy1Base +{ +} + +public interface IMy1<H> : IMy1Base +{ +} + +internal interface IMy2<T> +{ +} + +internal interface IMy2<T1, T2> +{ +} + +internal interface IMy3 +{ +} + +internal interface @internal +{ +} + +public abstract class TestClassBase : IMy3 +{ +} + +[GenerateAutoAs(EntireInterfaceHierarchy = false, SkipSystemInterfaces = true)] +public partial class TestClass<T> : TestClassBase, IMy1<T>, IMy2<int>, IMy2<string>, IMy2<string, string>, IEnumerable<int>, @internal +{ + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public IEnumerator<int> GetEnumerator() + { + throw new NotImplementedException(); + } + + //internal IMy2<int> AsMy2_0() => this; +} diff --git a/BeaKona.AutoAsAttributes/BeaKona.AutoAsAttributes.csproj b/BeaKona.AutoAsAttributes/BeaKona.AutoAsAttributes.csproj new file mode 100644 index 0000000..0f49060 --- /dev/null +++ b/BeaKona.AutoAsAttributes/BeaKona.AutoAsAttributes.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>latest</LangVersion> + <Nullable>enable</Nullable> + <PackageId>BeaKona.AutoAsAttributes</PackageId> + <Authors>BeaKona</Authors> + <Description>Shared BeaKona.AutoAs source generator attributes.</Description> + <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> + <PackageLicenseExpression>MIT</PackageLicenseExpression> + <RepositoryUrl>https://github.com/beakona/AutoAs</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <Version>1.0.0</Version> + <GeneratePackageOnBuild>False</GeneratePackageOnBuild> + </PropertyGroup> + +</Project> diff --git a/BeaKona.AutoAsAttributes/GenerateAutoAsAttribute.cs b/BeaKona.AutoAsAttributes/GenerateAutoAsAttribute.cs new file mode 100644 index 0000000..377aa9e --- /dev/null +++ b/BeaKona.AutoAsAttributes/GenerateAutoAsAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Diagnostics; + +namespace BeaKona; + +[Conditional("CodeGeneration")] +[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class GenerateAutoAsAttribute : Attribute +{ + public bool EntireInterfaceHierarchy { get; set; } = false; + public bool SkipSystemInterfaces { get; set; } = true; +} diff --git a/BeaKona.AutoAsGenerator/AutoAsResource.Designer.cs b/BeaKona.AutoAsGenerator/AutoAsResource.Designer.cs new file mode 100644 index 0000000..1a7e6ce --- /dev/null +++ b/BeaKona.AutoAsGenerator/AutoAsResource.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace BeaKona.AutoAsGenerator { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AutoAsResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AutoAsResource() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BeaKona.AutoAsGenerator.AutoAsResource", typeof(AutoAsResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to AA01.. + /// </summary> + internal static string AA01_description { + get { + return ResourceManager.GetString("AA01_description", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Type '{0}' is not marked as partial.. + /// </summary> + internal static string AA01_message { + get { + return ResourceManager.GetString("AA01_message", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Type is not marked as partial.. + /// </summary> + internal static string AA01_title { + get { + return ResourceManager.GetString("AA01_title", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to AA02.. + /// </summary> + internal static string AA02_description { + get { + return ResourceManager.GetString("AA02_description", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Type '{0}' is static.. + /// </summary> + internal static string AA02_message { + get { + return ResourceManager.GetString("AA02_message", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Type is static.. + /// </summary> + internal static string AA02_title { + get { + return ResourceManager.GetString("AA02_title", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to AA03.. + /// </summary> + internal static string AA03_description { + get { + return ResourceManager.GetString("AA03_description", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Only class, record or struct can auto-implement interface.. + /// </summary> + internal static string AA03_message { + get { + return ResourceManager.GetString("AA03_message", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Type is not a class, record, or struct.. + /// </summary> + internal static string AA03_title { + get { + return ResourceManager.GetString("AA03_title", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to AA04.. + /// </summary> + internal static string AA04_description { + get { + return ResourceManager.GetString("AA04_description", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Internal exception [{0}].. + /// </summary> + internal static string AA04_message { + get { + return ResourceManager.GetString("AA04_message", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Internal exception.. + /// </summary> + internal static string AA04_title { + get { + return ResourceManager.GetString("AA04_title", resourceCulture); + } + } + } +} diff --git a/BeaKona.AutoAsGenerator/AutoAsResource.resx b/BeaKona.AutoAsGenerator/AutoAsResource.resx new file mode 100644 index 0000000..177f26b --- /dev/null +++ b/BeaKona.AutoAsGenerator/AutoAsResource.resx @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="AA01_description" xml:space="preserve"> + <value>AA01.</value> + </data> + <data name="AA01_message" xml:space="preserve"> + <value>Type '{0}' is not marked as partial.</value> + </data> + <data name="AA01_title" xml:space="preserve"> + <value>Type is not marked as partial.</value> + </data> + <data name="AA02_description" xml:space="preserve"> + <value>AA02.</value> + </data> + <data name="AA02_message" xml:space="preserve"> + <value>Type '{0}' is static.</value> + </data> + <data name="AA02_title" xml:space="preserve"> + <value>Type is static.</value> + </data> + <data name="AA03_description" xml:space="preserve"> + <value>AA03.</value> + </data> + <data name="AA03_message" xml:space="preserve"> + <value>Only class, record or struct can auto-implement interface.</value> + </data> + <data name="AA03_title" xml:space="preserve"> + <value>Type is not a class, record, or struct.</value> + </data> + <data name="AA04_description" xml:space="preserve"> + <value>AA04.</value> + </data> + <data name="AA04_message" xml:space="preserve"> + <value>Internal exception [{0}].</value> + </data> + <data name="AA04_title" xml:space="preserve"> + <value>Internal exception.</value> + </data> +</root> \ No newline at end of file diff --git a/BeaKona.AutoAsGenerator/BeaKona.AutoAsGenerator.csproj b/BeaKona.AutoAsGenerator/BeaKona.AutoAsGenerator.csproj new file mode 100644 index 0000000..0621e65 --- /dev/null +++ b/BeaKona.AutoAsGenerator/BeaKona.AutoAsGenerator.csproj @@ -0,0 +1,52 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>latest</LangVersion> + <Nullable>enable</Nullable> + <IncludeBuildOutput>false</IncludeBuildOutput> + <PackageId>BeaKona.AutoAsGenerator</PackageId> + <Authors>BeaKona</Authors> + <Description>Fluent AsInterface() method C# source generator.</Description> + <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> + <PackageLicenseExpression>MIT</PackageLicenseExpression> + <RepositoryUrl>https://github.com/beakona/AutoAs</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput</TargetsForTfmSpecificContentInPackage> + <Version>1.0.0</Version> + <IsRoslynComponent>true</IsRoslynComponent> + <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CSharp" Version="4.7.0" PrivateAssets="all" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\BeaKona.AutoAsAttributes\BeaKona.AutoAsAttributes.csproj" PrivateAssets="all" /> + </ItemGroup> + + <ItemGroup> + <Compile Update="AutoAsResource.Designer.cs"> + <DesignTime>True</DesignTime> + <AutoGen>True</AutoGen> + <DependentUpon>AutoAsResource.resx</DependentUpon> + </Compile> + </ItemGroup> + + <ItemGroup> + <EmbeddedResource Update="AutoAsResource.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>AutoAsResource.Designer.cs</LastGenOutput> + </EmbeddedResource> + </ItemGroup> + <Target Name="_AddAnalyzersToOutput"> + <ItemGroup> + <TfmSpecificPackageFile Include="$(OutputPath)\BeaKona.AutoAsGenerator.dll" PackagePath="analyzers/dotnet/cs" Pack="true" Visible="false" /> + <TfmSpecificPackageFile Include="$(OutputPath)\BeaKona.AutoAsAttributes.dll" PackagePath="analyzers/dotnet/cs" Pack="true" Visible="false" /> + <TfmSpecificPackageFile Include="$(OutputPath)\BeaKona.AutoAsAttributes.dll" PackagePath="lib/netstandard2.0" Pack="true" Visible="true" /> + </ItemGroup> + </Target> + +</Project> diff --git a/BeaKona.AutoAsGenerator/CSharpCodeTextWriter.cs b/BeaKona.AutoAsGenerator/CSharpCodeTextWriter.cs new file mode 100644 index 0000000..27de71a --- /dev/null +++ b/BeaKona.AutoAsGenerator/CSharpCodeTextWriter.cs @@ -0,0 +1,388 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace BeaKona.AutoAsGenerator; + +internal sealed class CSharpCodeTextWriter : ICodeTextWriter +{ + public CSharpCodeTextWriter(GeneratorExecutionContext context, Compilation compilation) + { + this.Context = context; + this.Compilation = compilation; + } + + public GeneratorExecutionContext Context { get; } + public Compilation Compilation { get; } + + public void WriteTypeReference(SourceBuilder builder, ITypeSymbol type, ScopeInfo scope) + { + if (scope.TryGetAlias(type, out string? typeName)) + { + if (typeName != null) + { + builder.Append(typeName); + } + } + else + { + bool processed = false; + if (type.SpecialType != SpecialType.None) + { + processed = true; + switch (type.SpecialType) + { + default: processed = false; break; + case SpecialType.System_Object: builder.Append("object"); break; + case SpecialType.System_Void: builder.Append("void"); break; + case SpecialType.System_Boolean: builder.Append("bool"); break; + case SpecialType.System_Char: builder.Append("char"); break; + case SpecialType.System_SByte: builder.Append("sbyte"); break; + case SpecialType.System_Byte: builder.Append("byte"); break; + case SpecialType.System_Int16: builder.Append("short"); break; + case SpecialType.System_UInt16: builder.Append("ushort"); break; + case SpecialType.System_Int32: builder.Append("int"); break; + case SpecialType.System_UInt32: builder.Append("uint"); break; + case SpecialType.System_Int64: builder.Append("long"); break; + case SpecialType.System_UInt64: builder.Append("ulong"); break; + case SpecialType.System_Decimal: builder.Append("decimal"); break; + case SpecialType.System_Single: builder.Append("float"); break; + case SpecialType.System_Double: builder.Append("double"); break; + //case SpecialType.System_Half: builder.Append("half"); break; + case SpecialType.System_String: builder.Append("string"); break; + } + } + + if (processed == false) + { + if (type is IArrayTypeSymbol array) + { + this.WriteTypeReference(builder, array.ElementType, scope); + builder.Append('['); + for (int i = 1; i < array.Rank; i++) + { + builder.Append(','); + } + builder.Append(']'); + } + else + { + static bool IsTupleWithAliases(INamedTypeSymbol tuple) + { + return tuple.TupleElements.Any(i => i.CorrespondingTupleField != null && i.Equals(i.CorrespondingTupleField, SymbolEqualityComparer.Default) == false); + } + + if (type.IsTupleType && type is INamedTypeSymbol tupleType && IsTupleWithAliases(tupleType)) + { + builder.Append('('); + bool first = true; + foreach (IFieldSymbol field in tupleType.TupleElements) + { + if (first) + { + first = false; + } + else + { + builder.Append(", "); + } + this.WriteTypeReference(builder, field.Type, scope); + builder.Append(' '); + this.WriteIdentifier(builder, field); + } + builder.Append(')'); + } + else if (type is INamedTypeSymbol nt && SemanticFacts.IsNullableT(this.Compilation, nt)) + { + this.WriteTypeReference(builder, nt.TypeArguments[0], scope); + } + else + { + if (type is ITypeParameterSymbol == false) + { + if (type.Equals(scope.Type, SymbolEqualityComparer.Default) == false) + { + string? alias = SemanticFacts.ResolveAssemblyAlias(this.Compilation, type.ContainingAssembly); + ISymbol[] symbols; + if (alias == null) + { + symbols = SemanticFacts.GetRelativeSymbols(type, scope.Type); + } + else + { + symbols = SemanticFacts.GetContainingSymbols(type, false); + builder.Append(alias); + builder.Append("::"); + builder.RegisterAlias(alias); + } + + foreach (ISymbol symbol in symbols) + { + this.WriteIdentifier(builder, symbol); + + if (symbol is INamedTypeSymbol snt && snt.IsGenericType) + { + builder.Append('<'); + this.WriteTypeArgumentsDefinition(builder, snt.TypeArguments, scope); + builder.Append('>'); + } + + builder.Append('.'); + } + } + } + + this.WriteIdentifier(builder, type); + + { + if (type is INamedTypeSymbol tnt && tnt.IsGenericType) + { + builder.Append('<'); + this.WriteTypeArgumentsDefinition(builder, tnt.TypeArguments, scope); + builder.Append('>'); + } + } + } + } + } + } + + if (type.NullableAnnotation == NullableAnnotation.Annotated) + { + builder.Append('?'); + } + } + + public void WriteTypeArgumentsDefinition(SourceBuilder builder, IEnumerable<ITypeSymbol> typeArguments, ScopeInfo scope) + { + bool first = true; + foreach (ITypeSymbol t in typeArguments) + { + if (first) + { + first = false; + } + else + { + builder.Append(", "); + } + this.WriteTypeReference(builder, t, scope); + } + } + + public void WriteTypeDeclarationBeginning(SourceBuilder builder, INamedTypeSymbol type, ScopeInfo scope) + { + builder.Append("partial"); + builder.Append(' '); + if (type.TypeKind == TypeKind.Class) + { + bool isRecord = type.DeclaringSyntaxReferences.Any(i => i.GetSyntax() is RecordDeclarationSyntax); + builder.Append(isRecord ? "record" : "class"); + } + else if (type.TypeKind == TypeKind.Struct) + { + builder.Append("struct"); + } + else if (type.TypeKind == TypeKind.Interface) + { + builder.Append("interface"); + } + else + { + throw new NotSupportedException(nameof(WriteTypeDeclarationBeginning)); + } + builder.Append(' '); + this.WriteTypeReference(builder, type, scope); + } + + public void WriteNamespaceBeginning(SourceBuilder builder, INamespaceSymbol @namespace) + { + if (@namespace != null && @namespace.ConstituentNamespaces.Length > 0) + { + List<INamespaceSymbol> containingNamespaces = []; + for (INamespaceSymbol? ct = @namespace; ct != null && ct.IsGlobalNamespace == false; ct = ct.ContainingNamespace) + { + containingNamespaces.Insert(0, ct); + } + + if (containingNamespaces.Count > 0) + { + builder.AppendIndentation(); + builder.Append("namespace"); + builder.Append(' '); + builder.AppendLine(string.Join(".", containingNamespaces.Select(i => GetSourceIdentifier(i)))); + builder.AppendIndentation(); + builder.AppendLine('{'); + builder.IncrementIndentation(); + } + } + } + + public void WriteHolderReference(SourceBuilder builder, ISymbol member, ScopeInfo scope) + { + if (member.IsStatic) + { + this.WriteTypeReference(builder, member.ContainingType, scope); + } + else + { + builder.Append("this"); + } + } + + public void WriteIdentifier(SourceBuilder builder, ISymbol symbol) + { + builder.Append(this.GetSourceIdentifier(symbol)); + } + + #region helper members + + private string GetSourceIdentifier(ISymbol symbol) + { + if (symbol is IPropertySymbol propertySymbol && propertySymbol.IsIndexer) + { + return "this"; + } + else if (symbol is INamespaceSymbol ns) + { + return ns.Name; + //return string.Join("+", ns.ConstituentNamespaces.Select(i => i.Name)); + //return $"<{@namespace.Name};{symbol}>" + this.GetSourceIdentifier(@namespace.Name); + } + else if (symbol.DeclaringSyntaxReferences.Length == 0) + { + return symbol.Name; + } + else + { + foreach (SyntaxReference syntaxReference in symbol.DeclaringSyntaxReferences) + { + SyntaxNode syntax = syntaxReference.GetSyntax(); + if (syntax is BaseTypeDeclarationSyntax type) + { + return this.GetSourceIdentifier(type.Identifier); + } + else if (syntax is MethodDeclarationSyntax method) + { + return this.GetSourceIdentifier(method.Identifier); + } + else if (syntax is ParameterSyntax parameter) + { + return this.GetSourceIdentifier(parameter.Identifier); + } + else if (syntax is VariableDeclaratorSyntax variableDeclarator) + { + return this.GetSourceIdentifier(variableDeclarator.Identifier); + } + else if (syntax is VariableDeclarationSyntax variableDeclaration) + { + if (variableDeclaration.Variables.Any(i => i.Identifier.IsVerbatimIdentifier())) + { + return "@" + symbol; + } + else + { + return symbol.ToString(); + } + } + else if (syntax is BaseFieldDeclarationSyntax field) + { + if (field.Declaration.Variables.Any(i => i.Identifier.IsVerbatimIdentifier())) + { + return "@" + symbol; + } + else + { + return symbol.ToString(); + } + } + else if (syntax is PropertyDeclarationSyntax property) + { + return this.GetSourceIdentifier(property.Identifier); + } + else if (syntax is IndexerDeclarationSyntax) + { + throw new InvalidOperationException("trying to resolve indexer name"); + } + else if (syntax is EventDeclarationSyntax @event) + { + return this.GetSourceIdentifier(@event.Identifier); + } + else if (syntax is TypeParameterSyntax typeParameter) + { + return this.GetSourceIdentifier(typeParameter.Identifier); + } + else if (syntax is TupleTypeSyntax) + { + return symbol.Name; + } + else if (syntax is TupleElementSyntax tupleElement) + { + return this.GetSourceIdentifier(tupleElement.Identifier); + } + else if (syntax is NamespaceDeclarationSyntax @namespace) + { + throw new NotSupportedException(syntax.GetType().ToString()); + } + else + { + throw new NotSupportedException(syntax.GetType().ToString()); + } + } + + throw new NotSupportedException(); + } + } + + private string GetSourceIdentifier(SyntaxToken identifier) + { + if (identifier.IsVerbatimIdentifier()) + { + return "@" + identifier.ValueText; + } + else + { + return identifier.ValueText; + } + } + + private string GetSourceIdentifier(NameSyntax name) + { + if (name is SimpleNameSyntax simpleName) + { + return this.GetSourceIdentifier(simpleName.Identifier); + } + else if (name is QualifiedNameSyntax qualifiedName) + { + string left = this.GetSourceIdentifier(qualifiedName.Left); + string right = this.GetSourceIdentifier(qualifiedName.Right); + if (string.IsNullOrEmpty(left)) + { + if (string.IsNullOrEmpty(right)) + { + throw new NotSupportedException("both are null_or_empty."); + } + else + { + return right; + } + } + else + { + if (string.IsNullOrEmpty(right)) + { + return left; + } + else + { + return left + "." + right; + } + } + } + else + { + throw new NotSupportedException(name.GetType().ToString()); + } + } + + #endregion +} diff --git a/BeaKona.AutoAsGenerator/GenerateAutoAsSourceGenerator.cs b/BeaKona.AutoAsGenerator/GenerateAutoAsSourceGenerator.cs new file mode 100644 index 0000000..989d42e --- /dev/null +++ b/BeaKona.AutoAsGenerator/GenerateAutoAsSourceGenerator.cs @@ -0,0 +1,320 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace BeaKona.AutoAsGenerator; + +[Generator] +public sealed class GenerateAutoAsSourceGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + // Register a syntax receiver that will be created for each generation pass + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + Compilation compilation = context.Compilation; + if (compilation is CSharpCompilation) + { + //retrieve the populated receiver + if (context.SyntaxReceiver is SyntaxReceiver receiver) + { + // get newly bound attribute + if (compilation.GetTypeByMetadataName(typeof(GenerateAutoAsAttribute).FullName) is INamedTypeSymbol generateAutoAsAttributeSymbol) + { + GenerateAutoAsAttribute? GetGenerateAutoAsAttribute(INamedTypeSymbol type) + { + foreach (AttributeData attribute in type.GetAttributes()) + { + if (attribute.AttributeClass != null && attribute.AttributeClass.Equals(generateAutoAsAttributeSymbol, SymbolEqualityComparer.Default)) + { + var result = new GenerateAutoAsAttribute(); + + foreach (KeyValuePair<string, TypedConstant> arg in attribute.NamedArguments) + { + switch (arg.Key) + { + case nameof(GenerateAutoAsAttribute.EntireInterfaceHierarchy): + { + if (arg.Value.Value is bool b) + { + result.EntireInterfaceHierarchy = b; + } + } + break; + case nameof(GenerateAutoAsAttribute.SkipSystemInterfaces): + { + if (arg.Value.Value is bool b) + { + result.SkipSystemInterfaces = b; + } + } + break; + } + } + + return result; + } + } + + return null; + } + + var types = new List<INamedTypeSymbol>(); + + foreach (TypeDeclarationSyntax candidate in receiver.Candidates) + { + SemanticModel model = compilation.GetSemanticModel(candidate.SyntaxTree); + + if (model.GetDeclaredSymbol(candidate) is INamedTypeSymbol type) + { + if (GetGenerateAutoAsAttribute(type) is GenerateAutoAsAttribute attribute) + { + if (type.IsPartial() == false) + { + Helpers.ReportDiagnostic(context, "BKAA01", nameof(AutoAsResource.AA01_title), nameof(AutoAsResource.AA01_message), nameof(AutoAsResource.AA01_description), DiagnosticSeverity.Error, type, + type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + continue; + } + + if (type.IsStatic) + { + Helpers.ReportDiagnostic(context, "BKAA02", nameof(AutoAsResource.AA02_title), nameof(AutoAsResource.AA02_message), nameof(AutoAsResource.AA02_description), DiagnosticSeverity.Error, type, + type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + continue; + } + + if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct) + { + Helpers.ReportDiagnostic(context, "BKAA03", nameof(AutoAsResource.AA03_title), nameof(AutoAsResource.AA03_message), nameof(AutoAsResource.AA03_description), DiagnosticSeverity.Error, type); + continue; + } + + try + { + string? code = GenerateAutoAsSourceGenerator.ProcessClass(context, compilation, type, attribute); + if (code != null) + { + string name = type.Arity > 0 ? $"{type.Name}_{type.Arity}" : type.Name; +#if PEEK_1 + GeneratePreview(context, name, code); +#else + context.AddSource($"{name}_GenerateAutoAs.g.cs", SourceText.From(code, Encoding.UTF8)); +#endif + } + } + catch (Exception ex) + { + Helpers.ReportDiagnostic(context, "BKAA09", nameof(AutoAsResource.AA04_title), nameof(AutoAsResource.AA04_message), nameof(AutoAsResource.AA04_description), DiagnosticSeverity.Error, type, + ex.ToString().Replace("\r", "").Replace("\n", "")); + } + } + } + } + } + } + } + } + +#if PEEK_1 + private static void GeneratePreview(GeneratorExecutionContext context, string name, string code) + { + var output = new StringBuilder(); + output.AppendLine("namespace BeaKona.Output {"); + output.AppendLine($"public static class Debug_{name}"); + output.AppendLine("{"); + output.AppendLine($"public static readonly string Info = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String(\"{Convert.ToBase64String(Encoding.UTF8.GetBytes(code ?? ""))}\"));"); + output.AppendLine("}"); + output.AppendLine("}"); + context.AddSource($"Output_Debug_{name}.g.cs", SourceText.From(output.ToString(), Encoding.UTF8)); + } +#endif + + private static string? ProcessClass(GeneratorExecutionContext context, Compilation compilation, INamedTypeSymbol type, GenerateAutoAsAttribute attribute) + { + var scope = new ScopeInfo(type); + + List<INamedTypeSymbol> interfaces; + + if (attribute.EntireInterfaceHierarchy) + { + interfaces = type.AllInterfaces.Where(i => i.CanBeReferencedByName).ToList(); + } + else + { + interfaces = []; + + //interface list is small, we will not use HashSet here + for (var t = type; t != null; t = t.BaseType) + { + foreach (var @interface in t.Interfaces) + { + if (@interface.CanBeReferencedByName && interfaces.Contains(@interface) == false) + { + interfaces.Add(@interface); + } + } + } + } + + if (attribute.SkipSystemInterfaces) + { + for (int i = 0; i < interfaces.Count; i++) + { + var @interface = interfaces[i]; + + if (@interface.ContainingNamespace is INamespaceSymbol @namespace) + { + if (@namespace.FirstNonGlobalNamespace() is INamespaceSymbol first) + { + if (first.Name.Equals("System", StringComparison.InvariantCulture) || first.Name.StartsWith("System.", StringComparison.InvariantCulture)) + { + interfaces.RemoveAt(i--); + } + } + } + } + } + + var options = SourceBuilderOptions.Load(context, null); + var builder = new SourceBuilder(options); + + ICodeTextWriter writer = new CSharpCodeTextWriter(context, compilation); + bool anyReasonToEmitSourceFile = false; + bool error = false; + + builder.AppendLine("// <auto-generated />"); + //bool isNullable = compilation.Options.NullableContextOptions == NullableContextOptions.Enable; + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + writer.WriteNamespaceBeginning(builder, type.ContainingNamespace); + + List<INamedTypeSymbol> containingTypes = []; + for (INamedTypeSymbol? ct = type.ContainingType; ct != null; ct = ct.ContainingType) + { + containingTypes.Insert(0, ct); + } + + foreach (INamedTypeSymbol ct in containingTypes) + { + builder.AppendIndentation(); + writer.WriteTypeDeclarationBeginning(builder, ct, new ScopeInfo(ct)); + builder.AppendLine(); + builder.AppendIndentation(); + builder.AppendLine('{'); + builder.IncrementIndentation(); + } + + builder.AppendIndentation(); + writer.WriteTypeDeclarationBeginning(builder, type, scope); + builder.AppendLine(); + builder.AppendIndentation(); + builder.AppendLine('{'); + builder.IncrementIndentation(); + + if (interfaces.Any()) + { + var existingMethods = type.GetMembers().OfType<IMethodSymbol>().Where(i => i.MethodKind != MethodKind.ExplicitInterfaceImplementation).Select(i => i.Name).ToHashSet(); + + string GetModifiedMethodName(string methodName, int? index) + { + if (methodName.StartsWith("I", StringComparison.InvariantCulture) && methodName.Length > 1 && char.IsUpper(methodName[1])) + { + methodName = methodName.Substring(1); + } + if (index.HasValue) + { + methodName = $"{methodName}_{index.Value}"; + } + return "As" + methodName; + } + + void WriteMethod(INamedTypeSymbol @interface, int? index) + { + var desiredMethodName = GetModifiedMethodName(@interface.Name, index); + if (existingMethods.Add(desiredMethodName)) + { + anyReasonToEmitSourceFile = true; + + builder.AppendIndentation(); + builder.Append(@interface.DeclaredAccessibility == Accessibility.Internal ? "internal" : "public"); + builder.Append(' '); + writer.WriteTypeReference(builder, @interface, scope); + builder.Append(' '); + builder.Append(desiredMethodName); + builder.Append("() => "); + writer.WriteHolderReference(builder, type, scope); + builder.Append(';'); + builder.AppendLine(); + } + } + + foreach (var group in interfaces.GroupBy(i => i.Name)) + { + if (group.Count() == 1) + { + WriteMethod(group.First(), null); + } + else + { + int index = 0; + + foreach (var @interface in group.OrderBy(i => i.TypeArguments.Length).ThenBy(i => i.Name)) + { + WriteMethod(@interface, index++); + } + } + } + } + + builder.DecrementIndentation(); + builder.AppendIndentation(); + builder.Append('}'); + + for (int i = 0; i < containingTypes.Count; i++) + { + builder.AppendLine(); + builder.DecrementIndentation(); + builder.AppendIndentation(); + builder.Append('}'); + } + + if (type.ContainingNamespace != null && type.ContainingNamespace.ConstituentNamespaces.Length > 0) + { + builder.AppendLine(); + builder.DecrementIndentation(); + builder.AppendIndentation(); + builder.Append('}'); + } + + if (builder.Options.InsertFinalNewLine) + { + builder.AppendLine(); + } + + return error == false && anyReasonToEmitSourceFile ? builder.ToString() : null; + } + + /// <summary> + /// Created on demand before each generation pass + /// </summary> + private sealed class SyntaxReceiver : ISyntaxReceiver + { + public List<TypeDeclarationSyntax> Candidates { get; } = []; + + /// <summary> + /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + /// </summary> + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // any type with at least one attribute is a candidate for source generation + if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.AttributeLists.Count > 0) + { + this.Candidates.Add(typeDeclarationSyntax); + } + } + } +} diff --git a/BeaKona.AutoAsGenerator/Globals.cs b/BeaKona.AutoAsGenerator/Globals.cs new file mode 100644 index 0000000..edc4fbc --- /dev/null +++ b/BeaKona.AutoAsGenerator/Globals.cs @@ -0,0 +1,4 @@ +global using Microsoft.CodeAnalysis; +global using System; +global using System.Collections.Generic; +global using System.Linq; diff --git a/BeaKona.AutoAsGenerator/Helpers.cs b/BeaKona.AutoAsGenerator/Helpers.cs new file mode 100644 index 0000000..5247ac3 --- /dev/null +++ b/BeaKona.AutoAsGenerator/Helpers.cs @@ -0,0 +1,26 @@ +namespace BeaKona.AutoAsGenerator; +internal static class Helpers +{ + public static void ReportDiagnostic(GeneratorExecutionContext context, string id, string title, string message, string description, DiagnosticSeverity severity, SyntaxNode? node, params object?[] messageArgs) + { + Helpers.ReportDiagnostic(context, id, title, message, description, severity, node?.GetLocation(), messageArgs); + } + + public static void ReportDiagnostic(GeneratorExecutionContext context, string id, string title, string message, string description, DiagnosticSeverity severity, ISymbol? member, params object?[] messageArgs) + { + Helpers.ReportDiagnostic(context, id, title, message, description, severity, member != null && member.Locations.Length > 0 ? member.Locations[0] : null, messageArgs); + } + + public static void ReportDiagnostic(GeneratorExecutionContext context, string id, string title, string message, string description, DiagnosticSeverity severity, Location? location, params object?[] messageArgs) + { + var lTitle = new LocalizableResourceString(title, AutoAsResource.ResourceManager, typeof(AutoAsResource)); + var lMessage = new LocalizableResourceString(message, AutoAsResource.ResourceManager, typeof(AutoAsResource)); + var lDescription = new LocalizableResourceString(description, AutoAsResource.ResourceManager, typeof(AutoAsResource)); + var category = typeof(GenerateAutoAsSourceGenerator).Namespace; + var link = "https://github.com/beakona/AutoAs"; + + var dd = new DiagnosticDescriptor(id, lTitle, lMessage, category, severity, true, lDescription, link, WellKnownDiagnosticTags.NotConfigurable); + var d = Diagnostic.Create(dd, location, messageArgs); + context.ReportDiagnostic(d); + } +} diff --git a/BeaKona.AutoAsGenerator/ICodeTextWriter.cs b/BeaKona.AutoAsGenerator/ICodeTextWriter.cs new file mode 100644 index 0000000..b67e581 --- /dev/null +++ b/BeaKona.AutoAsGenerator/ICodeTextWriter.cs @@ -0,0 +1,18 @@ +namespace BeaKona.AutoAsGenerator; + +internal interface ICodeTextWriter +{ + Compilation Compilation { get; } + + void WriteTypeReference(SourceBuilder builder, ITypeSymbol type, ScopeInfo scope); + + void WriteTypeArgumentsDefinition(SourceBuilder builder, IEnumerable<ITypeSymbol> typeArguments, ScopeInfo scope); + void WriteTypeDeclarationBeginning(SourceBuilder builder, INamedTypeSymbol type, ScopeInfo scope); + + void WriteNamespaceBeginning(SourceBuilder builder, INamespaceSymbol @namespace); + + void WriteHolderReference(SourceBuilder builder, ISymbol member, ScopeInfo scope); + + void WriteIdentifier(SourceBuilder builder, ISymbol symbol); + +} diff --git a/BeaKona.AutoAsGenerator/IEnumerableExtensions.cs b/BeaKona.AutoAsGenerator/IEnumerableExtensions.cs new file mode 100644 index 0000000..b6625b2 --- /dev/null +++ b/BeaKona.AutoAsGenerator/IEnumerableExtensions.cs @@ -0,0 +1,8 @@ +namespace BeaKona.AutoAsGenerator; +internal static class IEnumerableExtensions +{ + public static HashSet<T> ToHashSet<T>(this IEnumerable<T> @this) + { + return @this != null ? new HashSet<T>(@this) : []; + } +} diff --git a/BeaKona.AutoAsGenerator/INamedTypeSymbolExtensions.cs b/BeaKona.AutoAsGenerator/INamedTypeSymbolExtensions.cs new file mode 100644 index 0000000..05eed70 --- /dev/null +++ b/BeaKona.AutoAsGenerator/INamedTypeSymbolExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace BeaKona.AutoAsGenerator; + +internal static class INamedTypeSymbolExtensions +{ + public static bool IsPartial(this ITypeSymbol @this) + { + foreach (SyntaxReference syntax in @this.DeclaringSyntaxReferences) + { + if (syntax.GetSyntax() is MemberDeclarationSyntax declaration) + { + if (declaration.Modifiers.Any(i => i.IsKind(SyntaxKind.PartialKeyword))) + { + return true; + } + } + } + + return false; + } +} diff --git a/BeaKona.AutoAsGenerator/INamespaceSymbolExtensions.cs b/BeaKona.AutoAsGenerator/INamespaceSymbolExtensions.cs new file mode 100644 index 0000000..b39e248 --- /dev/null +++ b/BeaKona.AutoAsGenerator/INamespaceSymbolExtensions.cs @@ -0,0 +1,16 @@ +namespace BeaKona.AutoAsGenerator; + +internal static class INamespaceSymbolExtensions +{ + public static INamespaceSymbol? FirstNonGlobalNamespace(this INamespaceSymbol @this) + { + INamespaceSymbol? last = null; + + for (var n = @this; n.IsGlobalNamespace == false; n = n.ContainingNamespace) + { + last = n; + } + + return last; + } +} diff --git a/BeaKona.AutoAsGenerator/ScopeInfo.cs b/BeaKona.AutoAsGenerator/ScopeInfo.cs new file mode 100644 index 0000000..04ccda7 --- /dev/null +++ b/BeaKona.AutoAsGenerator/ScopeInfo.cs @@ -0,0 +1,95 @@ +using System.Collections.Immutable; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace BeaKona.AutoAsGenerator; + +internal sealed class ScopeInfo +{ + public ScopeInfo(ITypeSymbol type) + { + this.Type = type; + this.usedTypeArguments = new HashSet<string>(ScopeInfo.AllTypeArguments(type).Select(i => i.Name)); + } + + public ScopeInfo(ScopeInfo parentScope) + { + this.Type = parentScope.Type; + this.usedTypeArguments = new HashSet<string>(parentScope.usedTypeArguments); + } + + public ITypeSymbol Type { get; } + + private readonly HashSet<string> usedTypeArguments; + private readonly Dictionary<ITypeSymbol, string> aliasTypeParameterNameByCanonicalType = new(SymbolEqualityComparer.Default); + + private static ImmutableList<ITypeSymbol> AllTypeArguments(ISymbol symbol) + { + List<ITypeSymbol> types = []; + + for (ISymbol s = symbol; s != null; s = s.ContainingType) + { + if (s is INamedTypeSymbol ts) + { + types.AddRange(ts.TypeArguments); + } + else if (s is IMethodSymbol m) + { + types.AddRange(m.TypeArguments); + } + } + + return types.ToImmutableList(); + } + + public void CreateAliases(ImmutableArray<ITypeSymbol> typeArguments) + { + foreach (ITypeSymbol t in typeArguments) + { + if (this.aliasTypeParameterNameByCanonicalType.ContainsKey(t)) + { + throw new InvalidOperationException(); + } + + if (this.usedTypeArguments.Contains(t.Name)) + { + if (ScopeInfo.TrySplitAsBaseNameAndInteger(t.Name, out string? baseName, out int? index) == false) + { + baseName = t.Name; + index = 0; + } + string typeName; + do + { + typeName = $"{baseName}{++index}"; + } + while (this.usedTypeArguments.Contains(typeName)); + this.aliasTypeParameterNameByCanonicalType.Add(t, typeName); + } + } + } + + public bool TryGetAlias(ITypeSymbol symbol, /*[NotNullWhen(true)]*/ out string? alias) + { + return this.aliasTypeParameterNameByCanonicalType.TryGetValue(symbol, out alias); + } + + private static readonly Regex rxSplitter = new(@"^\s*(?<n>\w+)(?<value>\d+)\s*$", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.CultureInvariant); + + private static bool TrySplitAsBaseNameAndInteger(string name, /*[NotNullWhen(true)]*/ out string? baseName, /*[NotNullWhen(true)]*/ out int? value) + { + Match m = rxSplitter.Match(name); + if (m.Success) + { + baseName = m.Groups["n"].Value; + value = int.Parse(m.Groups["v"].Value, CultureInfo.InvariantCulture); + return true; + } + else + { + baseName = null; + value = null; + return false; + } + } +} diff --git a/BeaKona.AutoAsGenerator/SemanticFacts.cs b/BeaKona.AutoAsGenerator/SemanticFacts.cs new file mode 100644 index 0000000..467b568 --- /dev/null +++ b/BeaKona.AutoAsGenerator/SemanticFacts.cs @@ -0,0 +1,88 @@ +namespace BeaKona.AutoAsGenerator; + +internal static class SemanticFacts +{ + public static string? ResolveAssemblyAlias(Compilation compilation, IAssemblySymbol assembly) + { + //MetadataReferenceProperties.GlobalAlias + if (compilation.GetMetadataReference(assembly) is MetadataReference mr) + { + foreach (string alias in mr.Properties.Aliases) + { + if (string.IsNullOrEmpty(alias) == false) + { + return alias; + } + } + } + + return null; + } + + public static ISymbol[] GetRelativeSymbols(ITypeSymbol type, ITypeSymbol scope) + { + ISymbol[] typeSymbols = GetContainingSymbols(type, false); + ISymbol[] scopeSymbols = GetContainingSymbols(scope, true); + + int count = Math.Min(typeSymbols.Length, scopeSymbols.Length); + for (int i = 0; i < count; i++) + { + if (typeSymbols[i].Equals(scopeSymbols[i], SymbolEqualityComparer.Default) == false) + { + int remaining = typeSymbols.Length - i; + if (remaining > 0) + { + ISymbol[] result = new ISymbol[remaining]; + Array.Copy(typeSymbols, i, result, 0, result.Length); + return result; + } + else + { + break; + } + } + } + + return []; + } + + public static ISymbol[] GetContainingSymbols(ITypeSymbol type, bool includeSelf) + { + List<ISymbol> symbols = []; + + for (ISymbol t = includeSelf ? type : type.ContainingSymbol; t != null; t = t.ContainingSymbol) + { + if (t is IModuleSymbol || t is IAssemblySymbol) + { + break; + } + if (t is INamespaceSymbol tn && tn.IsGlobalNamespace) + { + break; + } + symbols.Insert(0, t); + } + + return [.. symbols]; + } + + public static bool IsNullableT(Compilation compilation, INamedTypeSymbol type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (type.IsValueType) + { + if (type.IsGenericType && type.IsUnboundGenericType == false) + { + INamedTypeSymbol symbolNullableT = compilation.GetSpecialType(SpecialType.System_Nullable_T); + + return symbolNullableT.ConstructUnboundGenericType().Equals(type.ConstructUnboundGenericType(), SymbolEqualityComparer.Default); + } + } + + return false; + } +} diff --git a/BeaKona.AutoAsGenerator/SourceBuilder.cs b/BeaKona.AutoAsGenerator/SourceBuilder.cs new file mode 100644 index 0000000..790aba8 --- /dev/null +++ b/BeaKona.AutoAsGenerator/SourceBuilder.cs @@ -0,0 +1,226 @@ +using System.Text; + +namespace BeaKona.AutoAsGenerator; +internal sealed class SourceBuilder +{ + public SourceBuilder(SourceBuilderOptions options) + { + this.Options = options; + } + + private SourceBuilder(SourceBuilder owner, SourceBuilderOptions options) : this(options) + { + this.owner = owner; + } + + private readonly SourceBuilder? owner; + + public SourceBuilderOptions Options { get; } + + private readonly List<object> elements = []; + private readonly HashSet<string> aliases = []; + + public void Clear() + { + this.elements.Clear(); + } + + public void RegisterAlias(string alias) + { + if (this.owner != null) + { + this.owner.RegisterAlias(alias); + } + else + { + this.aliases.Add(alias); + } + } + + public void AppendLine() => this.elements.Add(new LineSeparatorMarker()); + + public void AppendLine(string? text) + { + if (text != null) + { + this.Append(text); + } + this.AppendLine(); + } + + public void AppendLine(char c) + { + this.Append(c); + this.AppendLine(); + } + + public void Append(string text) + { + if (text != null) + { + this.elements.Add(text); + } + } + + public void Append(object? value) + { + if (value != null) + { + this.Append(value.ToString()); + } + } + + public void Append(char c) => this.elements.Add(c); + + public void AppendSeparated(string text) + { + if (text != null) + { + this.AppendSpaceIfNecessary(); + this.Append(text); + } + } + + public void AppendSpaceIfNecessary() + { + this.elements.Add(new FlexibleSpaceMarker()); + } + + private int currentDepth = 0; + + public void IncrementIndentation() => this.currentDepth++; + + public void DecrementIndentation() + { + if (this.currentDepth > 0) + { + this.currentDepth--; + } + } + + public void AppendIndentation() + { + this.elements.Add(new IndentationMarker(this.currentDepth)); + } + + public SourceBuilder AppendNewBuilder(bool register = true) + { + var builder = new SourceBuilder(this, this.Options); + if (register) + { + this.elements.Add(builder); + } + return builder; + } + + public SourceBuilder AppendBuilder(Action<SourceBuilder> builder) + { + if (builder != null) + { + SourceBuilder sb = this.AppendNewBuilder(); + builder(sb); + } + return this; + } + + public override string ToString() + { + var text = new StringBuilder(); + foreach (string alias in this.aliases.OrderByDescending(i => i)) + { + text.Append("extern alias "); + text.Append(alias); + text.AppendLine(";"); + } + this.Write(text); + return text.ToString(); + } + + private void Write(StringBuilder builder) + { + Dictionary<int, string>? cache = null; + foreach (object? element in this.elements) + { + if (element != null) + { + switch (element) + { + default: throw new NotSupportedException(nameof(Write)); + case string text: + builder.Append(text); + break; + case char ch: + builder.Append(ch); + break; + case LineSeparatorMarker: + builder.Append(this.Options.NewLine); + break; + case IndentationMarker indentation: + { + cache ??= []; + int depth = indentation.Depth; + if (cache.TryGetValue(depth, out string value) == false) + { + var sb = new StringBuilder(); + for (int i = 0; i < depth; i++) + { + sb.Append(this.Options.Indentation); + } + cache[depth] = value = sb.ToString(); + } + builder.Append(value); + break; + } + case FlexibleSpaceMarker: + { + if (builder.Length > 0) + { + char c = builder[builder.Length - 1]; + switch (c) + { + case ' ': + case '\t': + case '\r': + case '\n': + case '(': + case '[': + case '{': + case ')': + case ']': + case '}': + case ';': + break; + default: + builder.Append(' '); + break; + } + } + + break; + } + case SourceBuilder sb: + sb.Append(builder); + break; + } + } + } + } + + private sealed class LineSeparatorMarker + { + } + + private sealed class IndentationMarker + { + public IndentationMarker(int depth) + { + this.Depth = depth; + } + + public readonly int Depth; + } + + private sealed class FlexibleSpaceMarker + { + } +} diff --git a/BeaKona.AutoAsGenerator/SourceBuilderOptions.cs b/BeaKona.AutoAsGenerator/SourceBuilderOptions.cs new file mode 100644 index 0000000..d961d80 --- /dev/null +++ b/BeaKona.AutoAsGenerator/SourceBuilderOptions.cs @@ -0,0 +1,54 @@ +using System.Globalization; + +namespace BeaKona.AutoAsGenerator; + +internal sealed class SourceBuilderOptions +{ + public string Indentation { get; set; } = " "; + public string NewLine { get; set; } = "\r\n"; + public bool InsertFinalNewLine { get; set; } = true; + + public static SourceBuilderOptions Load(GeneratorExecutionContext context, SyntaxTree? syntaxTree) + { + var builderOptions = new SourceBuilderOptions(); + + var configOptions = syntaxTree != null ? context.AnalyzerConfigOptions.GetOptions(syntaxTree) : context.AnalyzerConfigOptions.GlobalOptions; + + var character = ' '; + if (configOptions.TryGetValue("indent_style", out var indentStyle)) + { + character = indentStyle.Equals("space", StringComparison.OrdinalIgnoreCase) ? ' ' : '\t'; + } + + var indentSize = 4; + if (configOptions.TryGetValue("indent_size", out var indentSizeText) && int.TryParse(indentSizeText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var indentSizeValue)) + { + indentSize = indentSizeValue; + } + + builderOptions.Indentation = new string(character, indentSize); + + if (configOptions.TryGetValue("end_of_line", out var endOfLine)) + { + builderOptions.NewLine = (object)(endOfLine ?? "") switch + { + "" => "\r\n", + "native" => "\r\n", + "autocrlf" => "\r\n", + "cr" => "\r", + "lf" => "\n", + "crlf" => "\r\n", + "lfcr" => "\n\r", + "nel" => "\u0085", + _ => "\r\n", + }; + } + + if (configOptions.TryGetValue("insert_final_newline", out var insertFinalNewline)) + { + builderOptions.InsertFinalNewLine = insertFinalNewline.Equals("true", StringComparison.OrdinalIgnoreCase); + } + + return builderOptions; + } +}