diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..a271784 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "0.28.2", + "commands": [ + "dotnet-csharpier" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1bab644 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,568 @@ +# Version: 2.0.1 (Using https://semver.org/) +# Updated: 2020-12-11 +# See https://github.com/RehanSaeed/EditorConfig/releases for release notes. +# See https://github.com/RehanSaeed/EditorConfig for updates to this file. +# See http://EditorConfig.org for more information about .editorconfig files. + +########################################## +# Common Settings +########################################## + +# This file is the top-most EditorConfig file +root = true + +# All Files +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +########################################## +# File Extension Settings +########################################## + +# Visual Studio Solution Files +[*.sln] +indent_style = tab + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML Configuration Files +[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# JSON Files +[*.{json,json5,webmanifest}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.md] +trim_trailing_whitespace = false + +# Web Files +[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,svg,vue}] +indent_size = 2 + +# Batch Files +[*.{cmd,bat}] +end_of_line = crlf + +# Bash Files +[*.sh] +end_of_line = lf + +# Makefiles +[Makefile] +indent_style = tab + +########################################## +# Default .NET Code Style Severities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope +########################################## + +[*.{cs,csx,cake,vb,vbx}] +# Default Severity for all .NET Code Style rules below +dotnet_analyzer_diagnostic.severity = silent + +########################################## +# File Header (Uncomment to support file headers) +# https://docs.microsoft.com/visualstudio/ide/reference/add-file-header +########################################## + +# [*.{cs,csx,cake,vb,vbx}] +# file_header_template = \n© PROJECT-AUTHOR\n + +# SA1636: File header copyright text should match +# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project. +# dotnet_diagnostic.SA1636.severity = none + +########################################## +# .NET Language Conventions +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions +########################################## + +# .NET Code Style Settings +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings +[*.{cs,csx,cake,vb,vbx}] +# "this." and "Me." qualifiers +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me +#dotnet_style_qualification_for_field = true:warning +#dotnet_style_qualification_for_property = true:warning +#dotnet_style_qualification_for_method = true:warning +#dotnet_style_qualification_for_event = true:warning +# Language keywords instead of framework type names for type references +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +# Modifier preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers +dotnet_style_require_accessibility_modifiers = always:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning +dotnet_style_readonly_field = true:warning +# Parentheses preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = always_for_clarity:suggestion +# Expression-level preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion +dotnet_diagnostic.IDE0045.severity = suggestion +dotnet_style_prefer_conditional_expression_over_return = false:suggestion +dotnet_diagnostic.IDE0046.severity = suggestion +dotnet_style_prefer_compound_assignment = true:warning +# Null-checking preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +# Parameter preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences +dotnet_code_quality_unused_parameters = all:warning +# More style options (Undocumented) +# https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641 +dotnet_style_operator_placement_when_wrapping = end_of_line +# https://github.com/dotnet/roslyn/pull/40070 +dotnet_style_prefer_simplified_interpolation = true:warning + +# C# Code Style Settings +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings +[*.{cs,csx,cake}] +# Implicit and explicit types +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning +# Expression-bodied members +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_constructors = true:warning +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning +# Pattern matching +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +# Inlined variable declarations +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations +csharp_style_inlined_variable_declaration = true:warning +# Expression-level preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences +csharp_prefer_simple_default_expression = true:warning +# "Null" checking preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences +csharp_style_throw_expression = true:warning +csharp_style_conditional_delegate_call = true:warning +# Code block preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences +csharp_prefer_braces = true:warning +# Unused value preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences +csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion +dotnet_diagnostic.IDE0058.severity = suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +dotnet_diagnostic.IDE0059.severity = suggestion +# Index and range preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +# Miscellaneous preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +csharp_using_directive_placement = outside_namespace:warning +csharp_prefer_static_local_function = true:warning +csharp_prefer_simple_using_statement = true:suggestion +dotnet_diagnostic.IDE0063.severity = suggestion + +########################################## +# .NET Formatting Conventions +# https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions +########################################## + +# Organize usings +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives +dotnet_sort_system_directives_first = true +# Newline options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false +# Spacing options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_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_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false +# Wrapping options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +csharp_style_namespace_declarations = file_scoped + +########################################## +# .NET Naming Conventions +# https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions +########################################## + +[*.{cs,csx,cake,vb,vbx}] +dotnet_diagnostic.CA1000.severity = suggestion +dotnet_diagnostic.CA1001.severity = error +dotnet_diagnostic.CA1018.severity = error +dotnet_diagnostic.CA1036.severity = silent +dotnet_diagnostic.CA1051.severity = suggestion +dotnet_diagnostic.CA1068.severity = error +dotnet_diagnostic.CA1069.severity = error +dotnet_diagnostic.CA1304.severity = error +dotnet_diagnostic.CA1305.severity = suggestion +dotnet_diagnostic.CA1307.severity = suggestion +dotnet_diagnostic.CA1309.severity = suggestion +dotnet_diagnostic.CA1310.severity = error +dotnet_diagnostic.CA1507.severity = suggestion +dotnet_diagnostic.CA1513.severity = suggestion +dotnet_diagnostic.CA1707.severity = suggestion +dotnet_diagnostic.CA1708.severity = suggestion +dotnet_diagnostic.CA1711.severity = suggestion +dotnet_diagnostic.CA1716.severity = suggestion +dotnet_diagnostic.CA1720.severity = suggestion +dotnet_diagnostic.CA1725.severity = suggestion +dotnet_diagnostic.CA1805.severity = suggestion +dotnet_diagnostic.CA1816.severity = suggestion +dotnet_diagnostic.CA1822.severity = suggestion +dotnet_diagnostic.CA1825.severity = error +dotnet_diagnostic.CA1826.severity = silent +dotnet_diagnostic.CA1827.severity = error +dotnet_diagnostic.CA1829.severity = suggestion +dotnet_diagnostic.CA1834.severity = error +dotnet_diagnostic.CA1845.severity = suggestion +dotnet_diagnostic.CA1848.severity = suggestion +dotnet_diagnostic.CA1852.severity = suggestion +dotnet_diagnostic.CA1860.severity = silent +dotnet_diagnostic.CA2016.severity = suggestion +dotnet_diagnostic.CA2201.severity = error +dotnet_diagnostic.CA2206.severity = error +dotnet_diagnostic.CA2208.severity = error +dotnet_diagnostic.CA2211.severity = error +dotnet_diagnostic.CA2249.severity = error +dotnet_diagnostic.CA2251.severity = error +dotnet_diagnostic.CA2252.severity = none +dotnet_diagnostic.CA2254.severity = suggestion + +dotnet_diagnostic.CS0169.severity = error +dotnet_diagnostic.CS0219.severity = error +dotnet_diagnostic.CS0649.severity = suggestion +dotnet_diagnostic.CS1998.severity = error +dotnet_diagnostic.CS8602.severity = error +dotnet_diagnostic.CS8604.severity = error +dotnet_diagnostic.CS8618.severity = error +dotnet_diagnostic.CS0618.severity = suggestion +dotnet_diagnostic.CS1998.severity = error +dotnet_diagnostic.CS4014.severity = error +dotnet_diagnostic.CS8600.severity = error +dotnet_diagnostic.CS8603.severity = error +dotnet_diagnostic.CS8625.severity = error + +dotnet_diagnostic.BL0005.severity = suggestion + +dotnet_diagnostic.MVC1000.severity = suggestion + +dotnet_diagnostic.RZ10012.severity = error + +dotnet_diagnostic.IDE0004.severity = error # redundant cast +dotnet_diagnostic.IDE0005.severity = error +dotnet_diagnostic.IDE0007.severity = error # Use var +dotnet_diagnostic.IDE0011.severity = error # Use braces on if statements +dotnet_diagnostic.IDE0010.severity = silent # populate switch +dotnet_diagnostic.IDE0017.severity = suggestion # initialization can be simplified +dotnet_diagnostic.IDE0021.severity = silent # expression body for constructors +dotnet_diagnostic.IDE0022.severity = silent # expression body for methods +dotnet_diagnostic.IDE0023.severity = suggestion # use expression body for operators +dotnet_diagnostic.IDE0024.severity = silent # expression body for operators +dotnet_diagnostic.IDE0025.severity = suggestion # use expression body for properties +dotnet_diagnostic.IDE0027.severity = suggestion # Use expression body for accessors +dotnet_diagnostic.IDE0028.severity = silent # expression body for accessors +dotnet_diagnostic.IDE0032.severity = suggestion # Use auto property +dotnet_diagnostic.IDE0033.severity = error # prefer tuple name +dotnet_diagnostic.IDE0037.severity = suggestion # simplify anonymous type +dotnet_diagnostic.IDE0040.severity = error # modifiers required +dotnet_diagnostic.IDE0041.severity = error # simplify null +dotnet_diagnostic.IDE0042.severity = error # deconstruct variable +dotnet_diagnostic.IDE0044.severity = suggestion # make field only when possible +dotnet_diagnostic.IDE0047.severity = suggestion # parameter name +dotnet_diagnostic.IDE0051.severity = error # unused field +dotnet_diagnostic.IDE0052.severity = error # unused member +dotnet_diagnostic.IDE0053.severity = suggestion # lambda not needed +dotnet_diagnostic.IDE0055.severity = suggestion # Fix formatting +dotnet_diagnostic.IDE0057.severity = suggestion # substring can be simplified +dotnet_diagnostic.IDE0060.severity = suggestion # unused parameters +dotnet_diagnostic.IDE0061.severity = suggestion # local expression body +dotnet_diagnostic.IDE0062.severity = suggestion # local to static +dotnet_diagnostic.IDE0063.severity = error # simplify using +dotnet_diagnostic.IDE0066.severity = suggestion # switch expression +dotnet_diagnostic.IDE0072.severity = suggestion # Populate switch - forces population of all cases even when default specified +dotnet_diagnostic.IDE0078.severity = suggestion # use pattern matching +dotnet_diagnostic.IDE0090.severity = suggestion # new can be simplified +dotnet_diagnostic.IDE0130.severity = suggestion # namespace folder structure +dotnet_diagnostic.IDE0160.severity = silent # Use block namespaces ARE NOT required +dotnet_diagnostic.IDE0161.severity = error # Please use file namespaces +dotnet_diagnostic.IDE0200.severity = suggestion # lambda not needed +dotnet_diagnostic.IDE1006.severity = suggestion # Naming rule violation: These words cannot contain lower case characters +dotnet_diagnostic.IDE0260.severity = suggestion # Use pattern matching +dotnet_diagnostic.IDE0270.severity = suggestion # Null check simplifcation +dotnet_diagnostic.IDE0290.severity = error # Primary Constructor +dotnet_diagnostic.IDE0300.severity = suggestion # Collection +dotnet_diagnostic.IDE0305.severity = suggestion # Collection ToList + +dotnet_diagnostic.NX0001.severity = error +dotnet_diagnostic.NX0002.severity = silent +dotnet_diagnostic.NX0003.severity = silent + +########################################## +# Styles +########################################## + +# camel_case_style - Define the camelCase style +dotnet_naming_style.camel_case_style.capitalization = camel_case +# pascal_case_style - Define the PascalCase style +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# constant_case - Define the CONSTANT_CASE style +dotnet_naming_style.constant_case.capitalization = all_upper +dotnet_naming_style.constant_case.word_separator = _ +# first_upper_style - The first character must start with an upper-case character +dotnet_naming_style.first_upper_style.capitalization = first_word_upper +# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' +dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case +dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I +# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' +dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case +dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T +# disallowed_style - Anything that has this style applied is marked as disallowed +dotnet_naming_style.disallowed_style.capitalization = pascal_case +dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ +dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ +# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file +dotnet_naming_style.internal_error_style.capitalization = pascal_case +dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ +dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ + +# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' +dotnet_naming_style.underscore_camel_case_style.capitalization = camel_case +dotnet_naming_style.underscore_camel_case_style.required_prefix = _ + +########################################## +# .NET Design Guideline Field Naming Rules +# Naming rules for fields follow the .NET Framework design guidelines +# https://docs.microsoft.com/dotnet/standard/design-guidelines/index +########################################## + +# All public/protected/protected_internal constant fields must be constant_case +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = constant_case +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning + +# All public/protected/protected_internal static readonly fields must be constant_case +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = constant_case +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning + +# No other public/protected/protected_internal fields are allowed +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error + +########################################## +# StyleCop Field Naming Rules +# Naming rules for fields follow the StyleCop analyzers +# This does not override any rules using disallowed_style above +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers +########################################## + +# All constant fields must be constant_case +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = constant_case +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning + +# All static readonly fields must be constant_case +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = constant_case +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning + +# No non-private instance fields are allowed +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error + +# Private fields must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = underscore_camel_case_style +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning + +# Local variables must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = warning + +# This rule should never fire. However, it's included for at least two purposes: +# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. +# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error + + +########################################## +# Other Naming Rules +########################################## + +# All of the following must be PascalCase: +# - Namespaces +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Classes and Enumerations +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Delegates +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types +# - Constructors, Properties, Events, Methods +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members +dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property +dotnet_naming_rule.element_rule.symbols = element_group +dotnet_naming_rule.element_rule.style = pascal_case_style +dotnet_naming_rule.element_rule.severity = warning + +# Interfaces use PascalCase and are prefixed with uppercase 'I' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.interface_group.applicable_kinds = interface +dotnet_naming_rule.interface_rule.symbols = interface_group +dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style +dotnet_naming_rule.interface_rule.severity = warning + +# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter +dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group +dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style +dotnet_naming_rule.type_parameter_rule.severity = warning + +# Function parameters use camelCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters +dotnet_naming_symbols.parameters_group.applicable_kinds = parameter +dotnet_naming_rule.parameters_rule.symbols = parameters_group +dotnet_naming_rule.parameters_rule.style = camel_case_style +dotnet_naming_rule.parameters_rule.severity = warning + +########################################## +# License +########################################## +# The following applies as to the .editorconfig file ONLY, and is +# included below for reference, per the requirements of the license +# corresponding to this .editorconfig file. +# See: https://github.com/RehanSaeed/EditorConfig +# +# MIT License +# +# Copyright (c) 2017-2019 Muhammad Rehan Saeed +# Copyright (c) 2019 Henry Gabryjelski +# +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject +# to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +########################################## diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3413224 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" # search for actions - there are other options available + directory: "/" # search in .github/workflows under root `/` + schedule: + interval: "weekly" # check for action update every week \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1da29ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: "Check Build" + +on: + pull_request: + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Debug --no-restore + + - name: Test + run: dotnet test --no-build --configuration Debug --verbosity normal \ No newline at end of file diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index f18e6f2..3350d1e 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -6,29 +6,24 @@ on: jobs: deploy: runs-on: ubuntu-latest - env: - DOTNET_CLI_TELEMETRY_OPTOUT: true steps: - name: Checkout Project - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --configuration Release --no-restore - - - name: Test - run: dotnet test --no-build --configuration Release --verbosity normal + run: dotnet build --configuration Release --no-restore Speckle.InterfaceGenerator/Speckle.InterfaceGenerator.csproj - name: Pack - run: dotnet pack --no-build --configuration Release InterfaceGenerator/InterfaceGenerator.csproj --output . + run: dotnet pack --no-build --configuration Release Speckle.InterfaceGenerator/Speckle.InterfaceGenerator.csproj --output . - name: Push to nuget.org - run: dotnet nuget push *.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{secrets.NUGET_API_KEY}} --skip-duplicate \ No newline at end of file + run: dotnet nuget push *.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{secrets.CONNECTORS_NUGET_TOKEN }} --skip-duplicate \ No newline at end of file diff --git a/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj b/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj deleted file mode 100644 index db3e2fe..0000000 --- a/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - - false - - - - - - - - - - - - - - diff --git a/InterfaceGenerator.Tests/Partial/PartialClass.1.cs b/InterfaceGenerator.Tests/Partial/PartialClass.1.cs deleted file mode 100644 index 02ff0de..0000000 --- a/InterfaceGenerator.Tests/Partial/PartialClass.1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace InterfaceGenerator.Tests.Partial; - -[GenerateAutoInterface] -internal partial class PartialClass : IPartialClass -{ - -} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/Partial/PartialClass.2.cs b/InterfaceGenerator.Tests/Partial/PartialClass.2.cs deleted file mode 100644 index 9bcf3b9..0000000 --- a/InterfaceGenerator.Tests/Partial/PartialClass.2.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace InterfaceGenerator.Tests.Partial; - -internal partial class PartialClass -{ - public void SomeMethodThatShouldGenerate() - { - } -} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/PartialClassTests.cs b/InterfaceGenerator.Tests/PartialClassTests.cs deleted file mode 100644 index a290724..0000000 --- a/InterfaceGenerator.Tests/PartialClassTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using FluentAssertions; -using InterfaceGenerator.Tests.Partial; -using Xunit; - -namespace InterfaceGenerator.Tests; - -public class PartialClassTests -{ - [Fact] - public void GeneratesMethodFromOtherParts() - { - var tInterface = typeof(IPartialClass); - tInterface.GetMethods().Should().Contain(x => x.Name == nameof(PartialClass.SomeMethodThatShouldGenerate)); - } -} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/SameName/SameNameClass.1.cs b/InterfaceGenerator.Tests/SameName/SameNameClass.1.cs deleted file mode 100644 index e3906f2..0000000 --- a/InterfaceGenerator.Tests/SameName/SameNameClass.1.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ReSharper disable CheckNamespace -namespace InterfaceGenerator.Tests.SameName_1; - -/// -/// A class with the same name as . It exists to test if the generated source units have fully -/// qualified names. -/// -[GenerateAutoInterface] -internal class SameNameClass : ISameNameClass -{ - -} \ No newline at end of file diff --git a/InterfaceGenerator/AttributeDataExtensions.cs b/InterfaceGenerator/AttributeDataExtensions.cs deleted file mode 100644 index 084842a..0000000 --- a/InterfaceGenerator/AttributeDataExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Linq; -using Microsoft.CodeAnalysis; - -namespace InterfaceGenerator -{ - internal static class AttributeDataExtensions - { - public static string? GetNamedParamValue(this AttributeData attributeData, string paramName) - { - var pair = attributeData.NamedArguments.FirstOrDefault(x => x.Key == paramName); - return pair.Value.Value?.ToString(); - } - } -} \ No newline at end of file diff --git a/InterfaceGenerator/Attributes.cs b/InterfaceGenerator/Attributes.cs deleted file mode 100644 index 754b8ec..0000000 --- a/InterfaceGenerator/Attributes.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace InterfaceGenerator -{ - - internal class Attributes - { - public const string AttributesNamespace = nameof(InterfaceGenerator); - - public const string GenerateAutoInterfaceClassname = "GenerateAutoInterfaceAttribute"; - public const string AutoInterfaceIgnoreAttributeClassname = "AutoInterfaceIgnoreAttribute"; - - public const string VisibilityModifierPropName = "VisibilityModifier"; - public const string InterfaceNamePropName = "Name"; - - public static readonly string AttributesSourceCode = $@" - -using System; -using System.Diagnostics; - -#nullable enable - -namespace {AttributesNamespace} -{{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] - [Conditional(""CodeGeneration"")] - internal sealed class {GenerateAutoInterfaceClassname} : Attribute - {{ - public string? {VisibilityModifierPropName} {{ get; init; }} - public string? {InterfaceNamePropName} {{ get; init; }} - - public {GenerateAutoInterfaceClassname}() - {{ - }} - }} - - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false)] - [Conditional(""CodeGeneration"")] - internal sealed class {AutoInterfaceIgnoreAttributeClassname} : Attribute - {{ - }} -}} -"; - } -} \ No newline at end of file diff --git a/InterfaceGenerator/AutoInterfaceGenerator.cs b/InterfaceGenerator/AutoInterfaceGenerator.cs deleted file mode 100644 index e4ec264..0000000 --- a/InterfaceGenerator/AutoInterfaceGenerator.cs +++ /dev/null @@ -1,461 +0,0 @@ -using System; -using System.CodeDom.Compiler; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; - -namespace InterfaceGenerator -{ - [Generator] - public class AutoInterfaceGenerator : ISourceGenerator - { - private INamedTypeSymbol _generateAutoInterfaceAttribute = null!; - private INamedTypeSymbol _ignoreAttribute = null!; - - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - - #if DEBUG - if (!Debugger.IsAttached) - { - // sadly this is Windows only so as of now :( - Debugger.Launch(); - } - #endif - } - - public void Execute(GeneratorExecutionContext context) - { - try - { - ExecuteCore(context); - } - catch (Exception exception) - { - RaiseExceptionDiagnostic(context, exception); - } - } - - private static void RaiseExceptionDiagnostic(GeneratorExecutionContext context, Exception exception) - { - var descriptor = new DiagnosticDescriptor( - "InterfaceGenerator.CriticalError", - $"Exception thrown in InterfaceGenerator", - $"{exception.GetType().FullName} {exception.Message} {exception.StackTrace.Trim()}", - "InterfaceGenerator", - DiagnosticSeverity.Error, - true, - customTags: WellKnownDiagnosticTags.AnalyzerException); - - var diagnostic = Diagnostic.Create(descriptor, null); - - context.ReportDiagnostic(diagnostic); - } - - private void ExecuteCore(GeneratorExecutionContext context) - { - // setting the culture to invariant prevents errors such as emitting a decimal comma (0,1) instead of - // a decimal point (0.1) in certain cultures - var prevCulture = Thread.CurrentThread.CurrentCulture; - Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - - GenerateAttributes(context); - GenerateInterfaces(context); - - Thread.CurrentThread.CurrentCulture = prevCulture; - } - - private static void GenerateAttributes(GeneratorExecutionContext context) - { - context.AddSource( - Attributes.GenerateAutoInterfaceClassname, - SourceText.From(Attributes.AttributesSourceCode, Encoding.UTF8)); - } - - private void GenerateInterfaces(GeneratorExecutionContext context) - { - if (context.SyntaxReceiver is not SyntaxReceiver receiver) - { - return; - } - - var compilation = GetCompilation(context); - InitAttributes(compilation); - - var classSymbols = GetImplTypeSymbols(compilation, receiver); - - List classSymbolNames = new List(); - - foreach (var implTypeSymbol in classSymbols) - { - if (!implTypeSymbol.TryGetAttribute(_generateAutoInterfaceAttribute, out var attributes)) - { - continue; - } - - if(classSymbolNames.Contains(implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true))) - { - continue; // partial class, already added - } - - classSymbolNames.Add(implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true)); - - var attribute = attributes.Single(); - var source = SourceText.From(GenerateInterfaceCode(implTypeSymbol, attribute), Encoding.UTF8); - - context.AddSource($"{implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true)}_AutoInterface.g.cs", source); - } - } - - private static string InferVisibilityModifier(ISymbol implTypeSymbol, AttributeData attributeData) - { - string? result = attributeData.GetNamedParamValue(Attributes.VisibilityModifierPropName); - if (!string.IsNullOrEmpty(result)) - { - return result!; - } - - return implTypeSymbol.DeclaredAccessibility switch - { - Accessibility.Public => "public", - _ => "internal", - }; - } - - private static string InferInterfaceName(ISymbol implTypeSymbol, AttributeData attributeData) - { - return attributeData.GetNamedParamValue(Attributes.InterfaceNamePropName) ?? $"I{implTypeSymbol.Name}"; - } - - private string GenerateInterfaceCode(INamedTypeSymbol implTypeSymbol, AttributeData attributeData) - { - using var stream = new MemoryStream(); - var streamWriter = new StreamWriter(stream, Encoding.UTF8); - var codeWriter = new IndentedTextWriter(streamWriter, " "); - - var namespaceName = implTypeSymbol.ContainingNamespace.ToDisplayString(); - var interfaceName = InferInterfaceName(implTypeSymbol, attributeData); - var visibilityModifier = InferVisibilityModifier(implTypeSymbol, attributeData); - - codeWriter.WriteLine("namespace {0}", namespaceName); - codeWriter.WriteLine("{"); - - ++codeWriter.Indent; - WriteSymbolDocsIfPresent(codeWriter, implTypeSymbol); - codeWriter.Write("{0} partial interface {1}", visibilityModifier, interfaceName); - WriteTypeGenericsIfNeeded(codeWriter, implTypeSymbol); - codeWriter.WriteLine(); - codeWriter.WriteLine("{"); - - ++codeWriter.Indent; - GenerateInterfaceMemberDefinitions(codeWriter, implTypeSymbol); - --codeWriter.Indent; - - codeWriter.WriteLine("}"); - --codeWriter.Indent; - - codeWriter.WriteLine("}"); - - codeWriter.Flush(); - stream.Seek(0, SeekOrigin.Begin); - using var reader = new StreamReader(stream, Encoding.UTF8, true); - return reader.ReadToEnd(); - } - - private static void WriteTypeGenericsIfNeeded(TextWriter writer, INamedTypeSymbol implTypeSymbol) - { - if (!implTypeSymbol.IsGenericType) - { - return; - } - - writer.Write("<"); - writer.WriteJoin(", ", implTypeSymbol.TypeParameters.Select(x => x.Name)); - writer.Write(">"); - - WriteTypeParameterConstraints(writer, implTypeSymbol.TypeParameters); - } - - private void GenerateInterfaceMemberDefinitions(TextWriter writer, INamespaceOrTypeSymbol implTypeSymbol) - { - foreach (var member in implTypeSymbol.GetMembers()) - { - if (member.DeclaredAccessibility != Accessibility.Public || - member.HasAttribute(_ignoreAttribute)) - { - continue; - } - - GenerateInterfaceMemberDefinition(writer, member); - } - } - - private static void GenerateInterfaceMemberDefinition(TextWriter writer, ISymbol member) - { - switch (member) - { - case IPropertySymbol propertySymbol: - GeneratePropertyDefinition(writer, propertySymbol); - break; - case IMethodSymbol methodSymbol: - GenerateMethodDefinition(writer, methodSymbol); - break; - } - } - - private static void WriteSymbolDocsIfPresent(TextWriter writer, ISymbol symbol) - { - var xml = symbol.GetDocumentationCommentXml(); - if (string.IsNullOrWhiteSpace(xml)) - { - return; - } - - // omit the fist and last lines to skip the tag - - var reader = new StringReader(xml); - var lines = new List(); - - while (true) - { - var line = reader.ReadLine(); - if (line is null) - { - break; - } - - lines.Add(line); - } - - for (int i = 1; i < lines.Count - 1; i++) - { - var line = lines[i].TrimStart(); // for some reason, 4 spaces are inserted to the beginning of the line - writer.WriteLine("/// {0}", line); - } - } - - private static bool IsPublicOrInternal(ISymbol symbol) - { - return symbol.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal; - } - - private static void GeneratePropertyDefinition(TextWriter writer, IPropertySymbol propertySymbol) - { - if (propertySymbol.IsStatic) - { - return; - } - - bool hasPublicGetter = propertySymbol.GetMethod is not null && - IsPublicOrInternal(propertySymbol.GetMethod); - - bool hasPublicSetter = propertySymbol.SetMethod is not null && - IsPublicOrInternal(propertySymbol.SetMethod); - - if (!hasPublicGetter && !hasPublicSetter) - { - return; - } - - WriteSymbolDocsIfPresent(writer, propertySymbol); - - if (propertySymbol.IsIndexer) - { - writer.Write("{0} this[", propertySymbol.Type); - writer.WriteJoin(", ", propertySymbol.Parameters, WriteMethodParam); - writer.Write("] "); - } - else - { - writer.Write("{0} {1} ", propertySymbol.Type, propertySymbol.Name); // ex. int Foo - } - - writer.Write("{ "); - - if (hasPublicGetter) - { - writer.Write("get; "); - } - - if (hasPublicSetter) - { - if (propertySymbol.SetMethod!.IsInitOnly) - { - writer.Write("init; "); - } - else - { - writer.Write("set; "); - } - } - - writer.WriteLine("}"); - } - - private static void GenerateMethodDefinition(TextWriter writer, IMethodSymbol methodSymbol) - { - if (methodSymbol.MethodKind != MethodKind.Ordinary || methodSymbol.IsStatic) - { - return; - } - - if (methodSymbol.IsImplicitlyDeclared && methodSymbol.Name != "Deconstruct") - { - // omit methods that are auto generated by the compiler (eg. record's methods), - // except for the record Deconstruct method - return; - } - - WriteSymbolDocsIfPresent(writer, methodSymbol); - - writer.Write("{0} {1}", methodSymbol.ReturnType, methodSymbol.Name); // ex. int Foo - - if (methodSymbol.IsGenericMethod) - { - writer.Write("<"); - writer.WriteJoin(", ", methodSymbol.TypeParameters.Select(x => x.Name)); - writer.Write(">"); - } - - writer.Write("("); - writer.WriteJoin(", ", methodSymbol.Parameters, WriteMethodParam); - - writer.Write(")"); - - if (methodSymbol.IsGenericMethod) - { - WriteTypeParameterConstraints(writer, methodSymbol.TypeParameters); - } - - writer.WriteLine(";"); - } - - private static void WriteMethodParam(TextWriter writer, IParameterSymbol param) - { - if (param.IsParams) - { - writer.Write("params "); - } - - switch (param.RefKind) - { - case RefKind.Ref: - writer.Write("ref "); - break; - case RefKind.Out: - writer.Write("out "); - break; - case RefKind.In: - writer.Write("in "); - break; - } - - writer.Write(param.Type); - writer.Write(" "); - - if (StringExtensions.IsCSharpKeyword(param.Name)) - { - writer.Write("@"); - } - - writer.Write(param.Name); - - if (param.HasExplicitDefaultValue) - { - WriteParamExplicitDefaultValue(writer, param); - } - } - - private static void WriteParamExplicitDefaultValue(TextWriter writer, IParameterSymbol param) - { - if (param.ExplicitDefaultValue is null) - { - writer.Write(" = default"); - } - else - { - switch (param.Type.Name) - { - case nameof(String): - writer.Write(" = \"{0}\"", param.ExplicitDefaultValue); - break; - case nameof(Single): - writer.Write(" = {0}f", param.ExplicitDefaultValue); - break; - case nameof(Double): - writer.Write(" = {0}d", param.ExplicitDefaultValue); - break; - case nameof(Decimal): - writer.Write(" = {0}m", param.ExplicitDefaultValue); - break; - case nameof(Boolean): - writer.Write(" = {0}", param.ExplicitDefaultValue.ToString().ToLower()); - break; - case nameof(Nullable): - writer.Write(" = {0}", param.ExplicitDefaultValue.ToString().ToLower()); - break; - default: - writer.Write(" = {0}", param.ExplicitDefaultValue); - break; - } - } - } - - private static void WriteTypeParameterConstraints( - TextWriter writer, - IEnumerable typeParameters) - { - foreach (var typeParameter in typeParameters) - { - var constraints = typeParameter.EnumGenericConstraints().ToList(); - if (constraints.Count == 0) - { - break; - } - - writer.Write(" where {0} : ", typeParameter.Name); - writer.WriteJoin(", ", constraints); - } - } - - private void InitAttributes(Compilation compilation) - { - _generateAutoInterfaceAttribute = compilation.GetTypeByMetadataName( - $"{Attributes.AttributesNamespace}.{Attributes.GenerateAutoInterfaceClassname}")!; - - _ignoreAttribute = compilation.GetTypeByMetadataName( - $"{Attributes.AttributesNamespace}.{Attributes.AutoInterfaceIgnoreAttributeClassname}")!; - } - - private static IEnumerable GetImplTypeSymbols(Compilation compilation, SyntaxReceiver receiver) - { - return receiver.CandidateTypes.Select(candidate => GetTypeSymbol(compilation, candidate)); - } - - private static INamedTypeSymbol GetTypeSymbol(Compilation compilation, SyntaxNode type) - { - var model = compilation.GetSemanticModel(type.SyntaxTree); - var typeSymbol = model.GetDeclaredSymbol(type)!; - return (INamedTypeSymbol)typeSymbol; - } - - private static Compilation GetCompilation(GeneratorExecutionContext context) - { - var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions; - - var compilation = context.Compilation.AddSyntaxTrees( - CSharpSyntaxTree.ParseText( - SourceText.From(Attributes.AttributesSourceCode, Encoding.UTF8), options)); - - return compilation; - } - } -} \ No newline at end of file diff --git a/InterfaceGenerator/InterfaceGenerator.csproj b/InterfaceGenerator/InterfaceGenerator.csproj deleted file mode 100644 index 9da3776..0000000 --- a/InterfaceGenerator/InterfaceGenerator.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - netstandard2 - 9.0 - enable - 1.0.14 - - true - false - false - true - - - - R. David - InterfaceGenerator - A source generator that creates interfaces from implementations - https://github.com/daver32/InterfaceGenerator - https://github.com/daver32/InterfaceGenerator/blob/master/LICENSE - https://github.com/daver32/InterfaceGenerator - git - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - diff --git a/InterfaceGenerator/StringExtensions.cs b/InterfaceGenerator/StringExtensions.cs deleted file mode 100644 index 8859a69..0000000 --- a/InterfaceGenerator/StringExtensions.cs +++ /dev/null @@ -1,125 +0,0 @@ -namespace InterfaceGenerator -{ - internal static class StringExtensions - { - public static bool IsCSharpKeyword(string? name) - { - switch (name) - { - case "abstract": - case "add": - case "alias": - case "as": - case "ascending": - case "async": - case "await": - case "base": - case "bool": - case "break": - case "by": - case "byte": - case "case": - case "catch": - case "char": - case "checked": - case "class": - case "const": - case "continue": - case "decimal": - case "default": - case "delegate": - case "descending": - case "do": - case "double": - case "dynamic": - case "else": - case "enum": - case "equals": - case "event": - case "explicit": - case "extern": - case "false": - case "finally": - case "fixed": - case "float": - case "for": - case "foreach": - case "from": - case "get": - case "global": - case "goto": - // `group` is a contextual to linq queries that we don't generate - //case "group": - case "if": - case "implicit": - case "in": - case "int": - case "interface": - case "internal": - case "into": - case "is": - case "join": - case "let": - case "lock": - case "long": - case "nameof": - case "namespace": - case "new": - case "null": - case "object": - case "on": - case "operator": - // `orderby` is a contextual to linq queries that we don't generate - //case "orderby": - case "out": - case "override": - case "params": - case "partial": - case "private": - case "protected": - case "public": - case "readonly": - case "ref": - case "remove": - case "return": - case "sbyte": - case "sealed": - // `select` is a contextual to linq queries that we don't generate - // case "select": - case "set": - case "short": - case "sizeof": - case "stackalloc": - case "static": - case "string": - case "struct": - case "switch": - case "this": - case "throw": - case "true": - case "try": - case "typeof": - case "uint": - case "ulong": - case "unchecked": - case "unmanaged": - case "unsafe": - case "ushort": - case "using": - // `value` is a contextual to getters that we don't generate - // case "value": - case "var": - case "virtual": - case "void": - case "volatile": - case "when": - case "where": - case "while": - case "yield": - return true; - default: - return false; - } - } - } -} \ No newline at end of file diff --git a/InterfaceGenerator/SymbolExtensions.cs b/InterfaceGenerator/SymbolExtensions.cs deleted file mode 100644 index 5f309ae..0000000 --- a/InterfaceGenerator/SymbolExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; - -namespace InterfaceGenerator -{ - internal static class SymbolExtensions - { - public static bool TryGetAttribute( - this ISymbol symbol, - INamedTypeSymbol attributeType, - out IEnumerable attributes) - { - attributes = symbol.GetAttributes() - .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); - return attributes.Any(); - } - - public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) - { - return symbol.GetAttributes() - .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); - } - - //Ref: https://stackoverflow.com/questions/27105909/get-fully-qualified-metadata-name-in-roslyn - public static string GetFullMetadataName(this ISymbol symbol, bool useNameWhenNotFound = false) - { - if (IsRootNamespace(symbol)) - { - return useNameWhenNotFound ? symbol.Name : string.Empty; - } - - var stringBuilder = new StringBuilder(symbol.MetadataName); - var last = symbol; - - symbol = symbol.ContainingSymbol; - - while (!IsRootNamespace(symbol)) - { - if (symbol is ITypeSymbol && last is ITypeSymbol) - { - stringBuilder.Insert(0, '+'); - } - else - { - stringBuilder.Insert(0, '.'); - } - - stringBuilder.Insert(0, symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); - symbol = symbol.ContainingSymbol; - } - - var retVal = stringBuilder.ToString(); - if (string.IsNullOrWhiteSpace(retVal) && useNameWhenNotFound) - { - return symbol.Name; - } - - return retVal; - } - - private static bool IsRootNamespace(ISymbol symbol) - { - return symbol is INamespaceSymbol { IsGlobalNamespace: true }; - } - } -} \ No newline at end of file diff --git a/InterfaceGenerator/SyntaxReceiver.cs b/InterfaceGenerator/SyntaxReceiver.cs deleted file mode 100644 index 570b42b..0000000 --- a/InterfaceGenerator/SyntaxReceiver.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace InterfaceGenerator -{ - internal class SyntaxReceiver : ISyntaxReceiver - { - public IList CandidateTypes { get; } = new List(); - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && - IsClassOrRecord(typeDeclarationSyntax) && - typeDeclarationSyntax.AttributeLists.Count > 0) - { - CandidateTypes.Add(typeDeclarationSyntax); - } - } - - private static bool IsClassOrRecord(TypeDeclarationSyntax typeDeclarationSyntax) - { - return typeDeclarationSyntax is ClassDeclarationSyntax || typeDeclarationSyntax is RecordDeclarationSyntax; - } - } -} \ No newline at end of file diff --git a/InterfaceGenerator/TextWriterExtensions.cs b/InterfaceGenerator/TextWriterExtensions.cs deleted file mode 100644 index 2c25a61..0000000 --- a/InterfaceGenerator/TextWriterExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace InterfaceGenerator -{ - internal static class TextWriterExtensions - { - - public static void WriteJoin( - this TextWriter writer, - string separator, - IEnumerable values) - { - writer.WriteJoin(separator, values, (w, x) => w.Write(x)); - } - - public static void WriteJoin( - this TextWriter writer, - string separator, - IEnumerable values, - Action writeAction) - { - using var enumerator = values.GetEnumerator(); - - if (!enumerator.MoveNext()) - { - return; - } - - writeAction(writer, enumerator.Current); - - if (!enumerator.MoveNext()) - { - return; - } - - do - { - writer.Write(separator); - writeAction(writer, enumerator.Current); - } while (enumerator.MoveNext()); - } - } -} \ No newline at end of file diff --git a/InterfaceGenerator/TypeParameterSymbolExtensions.cs b/InterfaceGenerator/TypeParameterSymbolExtensions.cs deleted file mode 100644 index bfaa883..0000000 --- a/InterfaceGenerator/TypeParameterSymbolExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis; - -namespace InterfaceGenerator -{ - internal static class TypeParameterSymbolExtensions - { - public static IEnumerable EnumGenericConstraints(this ITypeParameterSymbol symbol) - { - // the class/struct/unmanaged/notnull constraint has to be the last - if (symbol.HasNotNullConstraint) - { - yield return "notnull"; - } - - if (symbol.HasValueTypeConstraint) - { - yield return "struct"; - } - - if (symbol.HasUnmanagedTypeConstraint) - { - yield return "unmanaged"; - } - - if (symbol.HasReferenceTypeConstraint) - { - yield return symbol.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated - ? "class?" - : "class"; - } - - - // types go in the middle - foreach (var constraintType in symbol.ConstraintTypes) - { - yield return constraintType.ToDisplayString(); - } - - - // the new() constraint has to be the last - if (symbol.HasConstructorConstraint) - { - yield return "new()"; - } - } - } -} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/AccessorsGenerationTests.cs b/Speckle.InterfaceGenerator.Tests/AccessorsGenerationTests.cs similarity index 50% rename from InterfaceGenerator.Tests/AccessorsGenerationTests.cs rename to Speckle.InterfaceGenerator.Tests/AccessorsGenerationTests.cs index 7e61ee2..f3bca43 100644 --- a/InterfaceGenerator.Tests/AccessorsGenerationTests.cs +++ b/Speckle.InterfaceGenerator.Tests/AccessorsGenerationTests.cs @@ -1,9 +1,13 @@ -using System.Runtime.CompilerServices; +using System; +using System.Collections.Generic; +using System.Diagnostics.SymbolStore; +using System.Linq; +using System.Runtime.CompilerServices; using FluentAssertions; using FluentAssertions.Common; using Xunit; -namespace InterfaceGenerator.Tests; +namespace Speckle.InterfaceGenerator.Tests; public class AccessorsGenerationTests { @@ -17,7 +21,11 @@ public AccessorsGenerationTests() [Fact] public void GetSetIndexer_IsImplemented() { - var indexer = typeof(IAccessorsTestsService).GetIndexerByParameterTypes(new[] { typeof(string) }); + var indexer = typeof(IAccessorsTestsService) + .GetProperties() + .First(x => + x.GetIndexParameters().Select(x => x.ParameterType).Contains(typeof(string)) + ); indexer.Should().NotBeNull(); @@ -31,8 +39,10 @@ public void GetSetIndexer_IsImplemented() [Fact] public void PublicProperty_IsImplemented() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(IAccessorsTestsService.PublicProperty))!; + var prop = + typeof(IAccessorsTestsService).GetProperty( + nameof(IAccessorsTestsService.PublicProperty) + ) ?? throw new InvalidOperationException(); prop.Should().NotBeNull(); @@ -46,15 +56,19 @@ public void PublicProperty_IsImplemented() [Fact] public void InitProperty_IsImplemented() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(IAccessorsTestsService.InitOnlyProperty))!; + var prop = + typeof(IAccessorsTestsService).GetProperty( + nameof(IAccessorsTestsService.InitOnlyProperty) + ) ?? throw new InvalidOperationException(); prop.Should().NotBeNull(); prop.GetMethod.Should().NotBeNull(); prop.SetMethod.Should().NotBeNull(); - prop.SetMethod!.ReturnParameter!.GetRequiredCustomModifiers().Should().Contain(typeof(IsExternalInit)); + prop.SetMethod?.ReturnParameter?.GetRequiredCustomModifiers() + .Should() + .Contain(typeof(IsExternalInit)); var _ = _sut.InitOnlyProperty; } @@ -62,8 +76,10 @@ public void InitProperty_IsImplemented() [Fact] public void PrivateSetter_IsOmitted() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(IAccessorsTestsService.PropertyWithPrivateSetter))!; + var prop = + typeof(IAccessorsTestsService).GetProperty( + nameof(IAccessorsTestsService.PropertyWithPrivateSetter) + ) ?? throw new InvalidOperationException(); prop.Should().NotBeNull(); @@ -76,8 +92,10 @@ public void PrivateSetter_IsOmitted() [Fact] public void PrivateGetter_IsOmitted() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(IAccessorsTestsService.PropertyWithPrivateGetter))!; + var prop = + typeof(IAccessorsTestsService).GetProperty( + nameof(IAccessorsTestsService.PropertyWithPrivateGetter) + ) ?? throw new InvalidOperationException(); prop.Should().NotBeNull(); @@ -90,8 +108,10 @@ public void PrivateGetter_IsOmitted() [Fact] public void ProtectedSetter_IsOmitted() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(IAccessorsTestsService.PropertyWithProtectedSetter))!; + var prop = + typeof(IAccessorsTestsService).GetProperty( + nameof(IAccessorsTestsService.PropertyWithProtectedSetter) + ) ?? throw new InvalidOperationException(); prop.Should().NotBeNull(); @@ -104,8 +124,10 @@ public void ProtectedSetter_IsOmitted() [Fact] public void ProtectedGetter_IsOmitted() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(IAccessorsTestsService.PropertyWithProtectedGetter))!; + var prop = + typeof(IAccessorsTestsService).GetProperty( + nameof(IAccessorsTestsService.PropertyWithProtectedGetter) + ) ?? throw new InvalidOperationException(); prop.Should().NotBeNull(); @@ -118,8 +140,9 @@ public void ProtectedGetter_IsOmitted() [Fact] public void IgnoredProperty_IsOmitted() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(AccessorsTestsService.IgnoredProperty)); + var prop = typeof(IAccessorsTestsService).GetProperty( + nameof(AccessorsTestsService.IgnoredProperty) + ); prop.Should().BeNull(); } @@ -127,8 +150,9 @@ public void IgnoredProperty_IsOmitted() [Fact] public void StaticProperty_IsOmitted() { - var prop = typeof(IAccessorsTestsService) - .GetProperty(nameof(AccessorsTestsService.StaticProperty)); + var prop = typeof(IAccessorsTestsService).GetProperty( + nameof(AccessorsTestsService.StaticProperty) + ); prop.Should().BeNull(); } @@ -141,25 +165,27 @@ internal class AccessorsTestsService : IAccessorsTestsService public int this[string x] { get => 0; - set - { - } + set { } } + public FtpStyleUriParser? SymbolBinder { get; set; } + public FtpStyleUriParser SymbolBinder2 { get; set; } = default!; + public IEnumerable SymbolBinder3 { get; set; } = default!; - public string PublicProperty { get; set; } + public string PublicProperty { get; set; } = string.Empty; - public string InitOnlyProperty { get; init; } + public string InitOnlyProperty { get; init; } = string.Empty; - public string PropertyWithPrivateSetter { get; private set; } + public string PropertyWithPrivateSetter { get; private set; } = string.Empty; - public string PropertyWithPrivateGetter { private get; set; } + public string PropertyWithPrivateGetter { private get; set; } = string.Empty; - public string PropertyWithProtectedSetter { get; protected set; } + public string PropertyWithProtectedSetter { get; protected set; } = string.Empty; - public string PropertyWithProtectedGetter { protected get; set; } + public string PropertyWithProtectedGetter { protected get; set; } = string.Empty; - [AutoInterfaceIgnore] public string IgnoredProperty { get; set; } + [AutoInterfaceIgnore] + public string IgnoredProperty { get; set; } = string.Empty; - public static string StaticProperty { get; set; } + public static string StaticProperty { get; set; } = string.Empty; } -// ReSharper enable UnusedMember.Local, ValueParameterNotUsed \ No newline at end of file +// ReSharper enable UnusedMember.Local, ValueParameterNotUsed diff --git a/InterfaceGenerator.Tests/GenericInterfaceTests.cs b/Speckle.InterfaceGenerator.Tests/GenericInterfaceTests.cs similarity index 78% rename from InterfaceGenerator.Tests/GenericInterfaceTests.cs rename to Speckle.InterfaceGenerator.Tests/GenericInterfaceTests.cs index 8642e3b..ca1255d 100644 --- a/InterfaceGenerator.Tests/GenericInterfaceTests.cs +++ b/Speckle.InterfaceGenerator.Tests/GenericInterfaceTests.cs @@ -3,7 +3,7 @@ using FluentAssertions; using Xunit; -namespace InterfaceGenerator.Tests; +namespace Speckle.InterfaceGenerator.Tests; public class GenericInterfaceTests { @@ -18,12 +18,15 @@ public void GenericParametersGeneratedCorrectly() genericArgs[0].IsClass.Should().BeTrue(); genericArgs[0] - .GenericParameterAttributes - .Should() + .GenericParameterAttributes.Should() .HaveFlag(GenericParameterAttributes.DefaultConstructorConstraint); var iEquatableOfTx = typeof(IEquatable<>).MakeGenericType(genericArgs[0]); - genericArgs[0].GetGenericParameterConstraints().Should().HaveCount(1).And.Contain(iEquatableOfTx); + genericArgs[0] + .GetGenericParameterConstraints() + .Should() + .HaveCount(1) + .And.Contain(iEquatableOfTx); genericArgs[1].IsValueType.Should().BeTrue(); } @@ -33,6 +36,4 @@ public void GenericParametersGeneratedCorrectly() // ReSharper disable once UnusedType.Global internal class GenericInterfaceTestsService : IGenericInterfaceTestsService where TX : class, IEquatable, new() - where TY : struct -{ -} \ No newline at end of file + where TY : struct { } diff --git a/InterfaceGenerator.Tests/MethodGenerationTests.cs b/Speckle.InterfaceGenerator.Tests/MethodGenerationTests.cs similarity index 62% rename from InterfaceGenerator.Tests/MethodGenerationTests.cs rename to Speckle.InterfaceGenerator.Tests/MethodGenerationTests.cs index fb9a748..263766b 100644 --- a/InterfaceGenerator.Tests/MethodGenerationTests.cs +++ b/Speckle.InterfaceGenerator.Tests/MethodGenerationTests.cs @@ -1,11 +1,12 @@ using System; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using FluentAssertions; using Xunit; -namespace InterfaceGenerator.Tests; +namespace Speckle.InterfaceGenerator.Tests; public class MethodGenerationTests { @@ -19,8 +20,9 @@ public MethodGenerationTests() [Fact] public void VoidMethod_IsImplemented() { - var method = typeof(IMethodsTestService).GetMethod( - nameof(MethodsTestService.VoidMethod))!; + var method = + typeof(IMethodsTestService).GetMethod(nameof(MethodsTestService.VoidMethod)) + ?? throw new InvalidOperationException(); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); @@ -34,8 +36,10 @@ public void VoidMethod_IsImplemented() [Fact] public void VoidMethodWithKeywordParam_IsImplemented() { - var method = typeof(IMethodsTestService).GetMethod( - nameof(MethodsTestService.VoidMethodWithKeywordParam))!; + var method = + typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithKeywordParam) + ) ?? throw new InvalidOperationException(); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); @@ -52,9 +56,11 @@ public void VoidMethodWithKeywordParam_IsImplemented() [Fact] public void VoidMethodWithParams_IsImplemented() { - var method = typeof(IMethodsTestService).GetMethod( - nameof(MethodsTestService.VoidMethodWithParams), - new[] { typeof(string), typeof(string) })!; + var method = + typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithParams), + [typeof(string), typeof(string)] + ) ?? throw new InvalidOperationException(); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); @@ -69,15 +75,20 @@ public void VoidMethodWithParams_IsImplemented() [Fact] public void VoidMethodWithOutParam_IsImplemented() { - var method = typeof(IMethodsTestService).GetMethod( - nameof(MethodsTestService.VoidMethodWithOutParam), - new[] { typeof(string).MakeByRefType() })!; + var method = + typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithOutParam), + [typeof(string).MakeByRefType()] + ) ?? throw new InvalidOperationException(); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); var parameters = method.GetParameters(); - parameters.Select(x => x.ParameterType).Should().AllBeEquivalentTo(typeof(string).MakeByRefType()); + parameters + .Select(x => x.ParameterType) + .Should() + .AllBeEquivalentTo(typeof(string).MakeByRefType()); parameters.Should().HaveCount(1); parameters[0].IsOut.Should().BeTrue(); @@ -87,15 +98,20 @@ public void VoidMethodWithOutParam_IsImplemented() [Fact] public void VoidMethodWithInParam_IsImplemented() { - var method = typeof(IMethodsTestService).GetMethod( - nameof(MethodsTestService.VoidMethodWithInParam), - new[] { typeof(string).MakeByRefType() })!; + var method = + typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithInParam), + [typeof(string).MakeByRefType()] + ) ?? throw new InvalidOperationException(); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); var parameters = method.GetParameters(); - parameters.Select(x => x.ParameterType).Should().AllBeEquivalentTo(typeof(string).MakeByRefType()); + parameters + .Select(x => x.ParameterType) + .Should() + .AllBeEquivalentTo(typeof(string).MakeByRefType()); parameters.Should().HaveCount(1); parameters[0].IsIn.Should().BeTrue(); @@ -106,15 +122,20 @@ public void VoidMethodWithInParam_IsImplemented() [Fact] public void VoidMethodWithRefParam_IsImplemented() { - var method = typeof(IMethodsTestService).GetMethod( - nameof(MethodsTestService.VoidMethodWithRefParam), - new[] { typeof(string).MakeByRefType() })!; + var method = + typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithRefParam), + [typeof(string).MakeByRefType()] + ) ?? throw new InvalidOperationException(); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); var parameters = method.GetParameters(); - parameters.Select(x => x.ParameterType).Should().AllBeEquivalentTo(typeof(string).MakeByRefType()); + parameters + .Select(x => x.ParameterType) + .Should() + .AllBeEquivalentTo(typeof(string).MakeByRefType()); parameters.Should().HaveCount(1); parameters[0].IsIn.Should().BeFalse(); parameters[0].IsOut.Should().BeFalse(); @@ -126,8 +147,9 @@ public void VoidMethodWithRefParam_IsImplemented() [Fact] public void StringMethod_IsImplemented() { - var method = typeof(IMethodsTestService).GetMethod( - nameof(MethodsTestService.StringMethod))!; + var method = + typeof(IMethodsTestService).GetMethod(nameof(MethodsTestService.StringMethod)) + ?? throw new InvalidOperationException(); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(string)); @@ -138,12 +160,42 @@ public void StringMethod_IsImplemented() var _ = _sut.StringMethod(); } + [Fact] + public void StringMethodNullable_IsImplemented() + { + var method = + typeof(IMethodsTestService).GetMethod(nameof(MethodsTestService.StringMethodNullable)) + ?? throw new InvalidOperationException(); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(string)); + IsNullable(method.ReturnType).Should().BeTrue(); + + var parameters = method.GetParameters(); + parameters.Should().BeEmpty(); + + var _ = _sut.StringMethod(); + } + + private static bool IsNullable(Type type) + { + var nullableContextAttribute = type.GetCustomAttribute(); + + // NullableContextAttribute exists and has a flag indicating nullable annotations + if (nullableContextAttribute != null && nullableContextAttribute.Flag == 1) + { + return true; + } + + return false; + } + [Fact] public void GenericVoidMethod_IsImplemented() { var method = typeof(IMethodsTestService) - .GetMethods() - .First(x => x.Name == nameof(MethodsTestService.GenericVoidMethod)); + .GetMethods() + .First(x => x.Name == nameof(MethodsTestService.GenericVoidMethod)); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); @@ -160,8 +212,8 @@ public void GenericVoidMethod_IsImplemented() public void GenericVoidMethodWithGenericParam_IsImplemented() { var method = typeof(IMethodsTestService) - .GetMethods() - .First(x => x.Name == nameof(MethodsTestService.GenericVoidMethodWithGenericParam)); + .GetMethods() + .First(x => x.Name == nameof(MethodsTestService.GenericVoidMethodWithGenericParam)); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); @@ -180,8 +232,8 @@ public void GenericVoidMethodWithGenericParam_IsImplemented() public void GenericVoidMethodWithConstraints_IsImplemented() { var method = typeof(IMethodsTestService) - .GetMethods() - .First(x => x.Name == nameof(MethodsTestService.GenericVoidMethodWithConstraints)); + .GetMethods() + .First(x => x.Name == nameof(MethodsTestService.GenericVoidMethodWithConstraints)); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); @@ -206,8 +258,8 @@ public void GenericVoidMethodWithConstraints_IsImplemented() public void VoidMethodWithOptionalParams_IsImplemented() { var method = typeof(IMethodsTestService) - .GetMethods() - .First(x => x.Name == nameof(MethodsTestService.VoidMethodWithOptionalParams)); + .GetMethods() + .First(x => x.Name == nameof(MethodsTestService.VoidMethodWithOptionalParams)); method.Should().NotBeNull(); method.ReturnType.Should().Be(typeof(void)); @@ -226,7 +278,7 @@ public void VoidMethodWithOptionalParams_IsImplemented() parameters[7].DefaultValue.Should().Be(true); parameters[8].DefaultValue.Should().Be(false); parameters[9].DefaultValue.Should().Be(null); - + _sut.VoidMethodWithOptionalParams(); } @@ -234,8 +286,8 @@ public void VoidMethodWithOptionalParams_IsImplemented() public void VoidMethodWithExpandingParam_IsImplemented() { var method = typeof(IMethodsTestService) - .GetMethods() - .First(x => x.Name == nameof(MethodsTestService.VoidMethodWithExpandingParam)); + .GetMethods() + .First(x => x.Name == nameof(MethodsTestService.VoidMethodWithExpandingParam)); method.ReturnType.Should().Be(typeof(void)); @@ -249,8 +301,8 @@ public void VoidMethodWithExpandingParam_IsImplemented() public void IgnoreMethod_IsOmitted() { var method = typeof(IMethodsTestService) - .GetMethods() - .FirstOrDefault(x => x.Name == nameof(MethodsTestService.IgnoredMethod)); + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.IgnoredMethod)); method.Should().BeNull(); } @@ -259,8 +311,8 @@ public void IgnoreMethod_IsOmitted() public void StaticMethod_IsOmitted() { var method = typeof(IMethodsTestService) - .GetMethods() - .FirstOrDefault(x => x.Name == nameof(MethodsTestService.StaticMethod)); + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.StaticMethod)); method.Should().BeNull(); } @@ -271,49 +323,38 @@ internal class MethodsTestService : IMethodsTestService { public const string StringConstant = "Const"; - public void VoidMethod() - { - } + public void VoidMethod() { } - public void VoidMethodWithParams(string a, string b) - { - } + public void VoidMethodWithParams(string a, string b) { } - public void VoidMethodWithKeywordParam(string @void) - { - } + public void VoidMethodWithKeywordParam(string @void) { } public void VoidMethodWithOutParam(out string a) { - a = default; + a = string.Empty; } - public void VoidMethodWithRefParam(ref string a) - { - } + public void VoidMethodWithRefParam(ref string a) { } - public void VoidMethodWithInParam(in string a) - { - } + public void VoidMethodWithInParam(in string a) { } public string StringMethod() { return string.Empty; } - public void GenericVoidMethod() + public string? StringMethodNullable() { + return null; } - public void GenericVoidMethodWithGenericParam(TX a) - { - } + public void GenericVoidMethod() { } + + public void GenericVoidMethodWithGenericParam(TX a) { } public void GenericVoidMethodWithConstraints() where TX : class - where TY : class, TX, new() - { - } + where TY : class, TX, new() { } public void VoidMethodWithOptionalParams( string stringLiteral = "cGFyYW0=", @@ -325,25 +366,17 @@ public void VoidMethodWithOptionalParams( bool falseLiteral = false, bool? nullableTrueLiteral = true, bool? nullableFalseLiteral = false, - bool? nullableNullBoolLiteral = null) - { - } + bool? nullableNullBoolLiteral = null + ) { } - public void VoidMethodWithExpandingParam(params string[] strings) - { - } + public void VoidMethodWithExpandingParam(params string[] strings) { } [AutoInterfaceIgnore] - public void IgnoredMethod() - { - } + public void IgnoredMethod() { } - public static void StaticMethod() - { - } + public static void StaticMethod() { } } [GenerateAutoInterface] -internal class MethodsTestServiceGeneric : IMethodsTestServiceGeneric where T : class -{ -} \ No newline at end of file +internal class MethodsTestServiceGeneric : IMethodsTestServiceGeneric + where T : class { } diff --git a/Speckle.InterfaceGenerator.Tests/Partial/PartialClass.1.cs b/Speckle.InterfaceGenerator.Tests/Partial/PartialClass.1.cs new file mode 100644 index 0000000..e965d62 --- /dev/null +++ b/Speckle.InterfaceGenerator.Tests/Partial/PartialClass.1.cs @@ -0,0 +1,4 @@ +namespace Speckle.InterfaceGenerator.Tests.Partial; + +[GenerateAutoInterface] +internal partial class PartialClass : IPartialClass { } diff --git a/Speckle.InterfaceGenerator.Tests/Partial/PartialClass.2.cs b/Speckle.InterfaceGenerator.Tests/Partial/PartialClass.2.cs new file mode 100644 index 0000000..caa08b1 --- /dev/null +++ b/Speckle.InterfaceGenerator.Tests/Partial/PartialClass.2.cs @@ -0,0 +1,6 @@ +namespace Speckle.InterfaceGenerator.Tests.Partial; + +internal partial class PartialClass +{ + public void SomeMethodThatShouldGenerate() { } +} diff --git a/Speckle.InterfaceGenerator.Tests/PartialClassTests.cs b/Speckle.InterfaceGenerator.Tests/PartialClassTests.cs new file mode 100644 index 0000000..ab64e34 --- /dev/null +++ b/Speckle.InterfaceGenerator.Tests/PartialClassTests.cs @@ -0,0 +1,18 @@ +using FluentAssertions; +using Speckle.InterfaceGenerator.Tests.Partial; +using Xunit; + +namespace Speckle.InterfaceGenerator.Tests; + +public class PartialClassTests +{ + [Fact] + public void GeneratesMethodFromOtherParts() + { + var tInterface = typeof(IPartialClass); + tInterface + .GetMethods() + .Should() + .Contain(x => x.Name == nameof(PartialClass.SomeMethodThatShouldGenerate)); + } +} diff --git a/InterfaceGenerator.Tests/RecordInterfaceGenerationTests.cs b/Speckle.InterfaceGenerator.Tests/RecordInterfaceGenerationTests.cs similarity index 57% rename from InterfaceGenerator.Tests/RecordInterfaceGenerationTests.cs rename to Speckle.InterfaceGenerator.Tests/RecordInterfaceGenerationTests.cs index 8274723..90e74fa 100644 --- a/InterfaceGenerator.Tests/RecordInterfaceGenerationTests.cs +++ b/Speckle.InterfaceGenerator.Tests/RecordInterfaceGenerationTests.cs @@ -1,8 +1,9 @@ +using System; using System.Runtime.CompilerServices; using FluentAssertions; using Xunit; -namespace InterfaceGenerator.Tests; +namespace Speckle.InterfaceGenerator.Tests; public class RecordInterfaceGenerationTests { @@ -16,14 +17,17 @@ public RecordInterfaceGenerationTests() [Fact] public void RecordProperty_IsGenerated() { - var prop = typeof(ITestRecord) - .GetProperty(nameof(TestRecord.RecordProperty))!; + var prop = + typeof(ITestRecord).GetProperty(nameof(TestRecord.RecordProperty)) + ?? throw new InvalidOperationException(); prop.Should().NotBeNull(); prop.GetMethod.Should().NotBeNull(); prop.SetMethod.Should().NotBeNull(); - prop.SetMethod!.ReturnParameter!.GetRequiredCustomModifiers().Should().Contain(typeof(IsExternalInit)); + prop.SetMethod?.ReturnParameter?.GetRequiredCustomModifiers() + .Should() + .Contain(typeof(IsExternalInit)); _sut.RecordProperty.Should().Be(420); } @@ -31,13 +35,12 @@ public void RecordProperty_IsGenerated() [Fact] public void RecordMethod_IsGenerated() { - var method = typeof(ITestRecord).GetMethod( - nameof(TestRecord.RecordMethod)); + var method = typeof(ITestRecord).GetMethod(nameof(TestRecord.RecordMethod)); method.Should().NotBeNull(); - method!.ReturnType.Should().Be(typeof(void)); + method?.ReturnType.Should().Be(typeof(void)); - var parameters = method.GetParameters(); + var parameters = method?.GetParameters(); parameters.Should().BeEmpty(); _sut.RecordMethod(); @@ -46,13 +49,12 @@ public void RecordMethod_IsGenerated() [Fact] public void Deconstruct_IsGenerated() { - var method = typeof(ITestRecord).GetMethod( - nameof(TestRecord.Deconstruct)); + var method = typeof(ITestRecord).GetMethod(nameof(TestRecord.Deconstruct)); method.Should().NotBeNull(); - method!.ReturnType.Should().Be(typeof(void)); + method?.ReturnType.Should().Be(typeof(void)); - var parameters = method.GetParameters(); + var parameters = method?.GetParameters() ?? throw new InvalidOperationException(); parameters.Length.Should().Be(1); var parameter = parameters[0]; @@ -64,7 +66,5 @@ public void Deconstruct_IsGenerated() [GenerateAutoInterface] internal record TestRecord(int RecordProperty) : ITestRecord { - public void RecordMethod() - { - } -} \ No newline at end of file + public void RecordMethod() { } +} diff --git a/Speckle.InterfaceGenerator.Tests/SameName/SameNameClass.1.cs b/Speckle.InterfaceGenerator.Tests/SameName/SameNameClass.1.cs new file mode 100644 index 0000000..e134043 --- /dev/null +++ b/Speckle.InterfaceGenerator.Tests/SameName/SameNameClass.1.cs @@ -0,0 +1,29 @@ +// ReSharper disable CheckNamespace + +using System; +using System.Collections.Generic; +using System.Diagnostics.SymbolStore; +using Speckle.InterfaceGenerator; + +namespace InterfaceGenerator.Tests.SameName_1; + +/// +/// A class with the same name as . It exists to test if the generated source units have fully +/// qualified names. +/// +[GenerateAutoInterface] +public class SameNameClass : ISameNameClass { } + +[GenerateAutoInterface] +public class SameNameClass2 : ISameNameClass2 +{ + public ISameNameClass Return() => throw new InvalidOperationException(); + + public SymbolToken Return2() => throw new InvalidOperationException(); + + public T GetRequiredService() + where T : class => throw new InvalidOperationException(); + + public void TestGenericParameter(List x) => + throw new InvalidOperationException(); +} diff --git a/InterfaceGenerator.Tests/SameName/SameNameClass.2.cs b/Speckle.InterfaceGenerator.Tests/SameName/SameNameClass.2.cs similarity index 55% rename from InterfaceGenerator.Tests/SameName/SameNameClass.2.cs rename to Speckle.InterfaceGenerator.Tests/SameName/SameNameClass.2.cs index 9147b4f..b1b2764 100644 --- a/InterfaceGenerator.Tests/SameName/SameNameClass.2.cs +++ b/Speckle.InterfaceGenerator.Tests/SameName/SameNameClass.2.cs @@ -1,8 +1,8 @@ // ReSharper disable CheckNamespace + +using Speckle.InterfaceGenerator; + namespace InterfaceGenerator.Tests.SameName_2; [GenerateAutoInterface] -internal class SameNameClass : ISameNameClass -{ - -} \ No newline at end of file +internal class SameNameClass : ISameNameClass { } diff --git a/Speckle.InterfaceGenerator.Tests/Speckle.InterfaceGenerator.Tests.csproj b/Speckle.InterfaceGenerator.Tests/Speckle.InterfaceGenerator.Tests.csproj new file mode 100644 index 0000000..1115338 --- /dev/null +++ b/Speckle.InterfaceGenerator.Tests/Speckle.InterfaceGenerator.Tests.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + false + Speckle.InterfaceGenerator.Tests + enable + true + + + + + + + + + + + + + + diff --git a/InterfaceGenerator.Tests/VisibilityModifierTests.cs b/Speckle.InterfaceGenerator.Tests/VisibilityModifierTests.cs similarity index 90% rename from InterfaceGenerator.Tests/VisibilityModifierTests.cs rename to Speckle.InterfaceGenerator.Tests/VisibilityModifierTests.cs index 33c9e08..04cc2a2 100644 --- a/InterfaceGenerator.Tests/VisibilityModifierTests.cs +++ b/Speckle.InterfaceGenerator.Tests/VisibilityModifierTests.cs @@ -2,7 +2,7 @@ using FluentAssertions; using Xunit; -namespace InterfaceGenerator.Tests; +namespace Speckle.InterfaceGenerator.Tests; public class VisibilityModifierTests { @@ -36,21 +36,13 @@ public void IImplicitlyInternalService_IsInternal() } [GenerateAutoInterface(VisibilityModifier = "public")] -internal class ExplicitlyPublicService : IExplicitlyPublicService -{ -} +internal class ExplicitlyPublicService : IExplicitlyPublicService { } [GenerateAutoInterface(VisibilityModifier = "internal")] -public class ExplicitlyInternalService : IExplicitlyInternalService -{ -} +public class ExplicitlyInternalService : IExplicitlyInternalService { } [GenerateAutoInterface] -public class ImplicitlyPublicService : IImplicitlyPublicService -{ -} +public class ImplicitlyPublicService : IImplicitlyPublicService { } [GenerateAutoInterface] -internal class ImplicitlyInternalService : IImplicitlyInternalService -{ -} \ No newline at end of file +internal class ImplicitlyInternalService : IImplicitlyInternalService { } diff --git a/InterfaceGenerator.sln b/Speckle.InterfaceGenerator.sln similarity index 57% rename from InterfaceGenerator.sln rename to Speckle.InterfaceGenerator.sln index 3cae7ca..e4ead48 100644 --- a/InterfaceGenerator.sln +++ b/Speckle.InterfaceGenerator.sln @@ -1,8 +1,16 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceGenerator", "InterfaceGenerator\InterfaceGenerator.csproj", "{D40E2D60-5580-42D2-8316-F5AAA42CFBF6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.InterfaceGenerator", "Speckle.InterfaceGenerator\Speckle.InterfaceGenerator.csproj", "{D40E2D60-5580-42D2-8316-F5AAA42CFBF6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceGenerator.Tests", "InterfaceGenerator.Tests\InterfaceGenerator.Tests.csproj", "{D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.InterfaceGenerator.Tests", "Speckle.InterfaceGenerator.Tests\Speckle.InterfaceGenerator.Tests.csproj", "{D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{47654DE0-162F-4045-B5EB-151C3678430C}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + global.json = global.json + .github\workflows\nuget.yml = .github\workflows\nuget.yml + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/InterfaceGenerator/.gitattributes b/Speckle.InterfaceGenerator/.gitattributes similarity index 100% rename from InterfaceGenerator/.gitattributes rename to Speckle.InterfaceGenerator/.gitattributes diff --git a/Speckle.InterfaceGenerator/AttributeDataExtensions.cs b/Speckle.InterfaceGenerator/AttributeDataExtensions.cs new file mode 100644 index 0000000..3d7c0ec --- /dev/null +++ b/Speckle.InterfaceGenerator/AttributeDataExtensions.cs @@ -0,0 +1,13 @@ +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Speckle.InterfaceGenerator; + +internal static class AttributeDataExtensions +{ + public static string? GetNamedParamValue(this AttributeData attributeData, string paramName) + { + var pair = attributeData.NamedArguments.FirstOrDefault(x => x.Key == paramName); + return pair.Value.Value?.ToString(); + } +} diff --git a/Speckle.InterfaceGenerator/Attributes.cs b/Speckle.InterfaceGenerator/Attributes.cs new file mode 100644 index 0000000..3d70ea1 --- /dev/null +++ b/Speckle.InterfaceGenerator/Attributes.cs @@ -0,0 +1,45 @@ +namespace Speckle.InterfaceGenerator; + +internal class Attributes +{ + public const string AttributesNamespace = "Speckle.InterfaceGenerator"; + + public const string GenerateAutoInterfaceClassname = "GenerateAutoInterfaceAttribute"; + public const string AutoInterfaceIgnoreAttributeClassname = "AutoInterfaceIgnoreAttribute"; + + public const string VisibilityModifierPropName = "VisibilityModifier"; + public const string InterfaceNamePropName = "Name"; + + public static readonly string AttributesSourceCode = + $@" + +#pragma warning disable IDE0005 +using System; +using System.Diagnostics; + +#nullable enable + +namespace {AttributesNamespace} +{{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] + [Conditional(""CodeGeneration"")] + internal sealed class {GenerateAutoInterfaceClassname} : Attribute + {{ + public string? {VisibilityModifierPropName} {{ get; init; }} + public string? {InterfaceNamePropName} {{ get; init; }} + + public {GenerateAutoInterfaceClassname}() + {{ + }} + }} + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false)] + [Conditional(""CodeGeneration"")] + internal sealed class {AutoInterfaceIgnoreAttributeClassname} : Attribute + {{ + }} +}} + +#pragma warning restore IDE0005 +"; +} diff --git a/Speckle.InterfaceGenerator/AutoInterfaceGenerator.cs b/Speckle.InterfaceGenerator/AutoInterfaceGenerator.cs new file mode 100644 index 0000000..2fc3394 --- /dev/null +++ b/Speckle.InterfaceGenerator/AutoInterfaceGenerator.cs @@ -0,0 +1,522 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace Speckle.InterfaceGenerator; + +[Generator] +public class AutoInterfaceGenerator : ISourceGenerator +{ + private INamedTypeSymbol? _generateAutoInterfaceAttribute; + private INamedTypeSymbol? _ignoreAttribute; + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + try + { + ExecuteCore(context); + } + catch (Exception exception) + { + RaiseExceptionDiagnostic(context, exception); + } + } + + private static void RaiseExceptionDiagnostic( + GeneratorExecutionContext context, + Exception exception + ) + { + var descriptor = new DiagnosticDescriptor( + "Speckle.InterfaceGenerator.CriticalError", + "Exception thrown in InterfaceGenerator", + $"{exception.GetType().FullName} {exception.Message} {exception.StackTrace.Trim()}", + "Speckle.InterfaceGenerator", + DiagnosticSeverity.Error, + true, + customTags: WellKnownDiagnosticTags.AnalyzerException + ); + + var diagnostic = Diagnostic.Create(descriptor, null); + + context.ReportDiagnostic(diagnostic); + } + + private void ExecuteCore(GeneratorExecutionContext context) + { + // setting the culture to invariant prevents errors such as emitting a decimal comma (0,1) instead of + // a decimal point (0.1) in certain cultures + var prevCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + + GenerateAttributes(context); + GenerateInterfaces(context); + + Thread.CurrentThread.CurrentCulture = prevCulture; + } + + private static void GenerateAttributes(GeneratorExecutionContext context) + { + context.AddSource( + $"{Attributes.GenerateAutoInterfaceClassname}.g.cs", + SourceText.From(Attributes.AttributesSourceCode, Encoding.UTF8) + ); + } + + private void GenerateInterfaces(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not SyntaxReceiver receiver) + { + return; + } + + var compilation = GetCompilation(context); + InitAttributes(compilation); + + var classSymbols = GetImplTypeSymbols(compilation, receiver); + + List classSymbolNames = []; + + foreach (var implTypeSymbol in classSymbols) + { + if ( + !implTypeSymbol.TryGetAttribute( + _generateAutoInterfaceAttribute + ?? throw new NullReferenceException( + "_generateAutoInterfaceAttribute is null" + ), + out var attributes + ) + ) + { + continue; + } + + if ( + classSymbolNames.Contains( + implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true) + ) + ) + { + continue; // partial class, already added + } + + classSymbolNames.Add(implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true)); + + var attribute = attributes.Single(); + var source = SourceText.From( + GenerateInterfaceCode(implTypeSymbol, attribute), + Encoding.UTF8 + ); + + context.AddSource( + $"{implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true)}_AutoInterface.g.cs", + source + ); + } + } + + private static string InferVisibilityModifier( + ISymbol implTypeSymbol, + AttributeData attributeData + ) + { + string? result = attributeData.GetNamedParamValue(Attributes.VisibilityModifierPropName); + if (!string.IsNullOrEmpty(result)) + { + return result ?? throw new NullReferenceException("result is null"); + } + + return implTypeSymbol.DeclaredAccessibility switch + { + Accessibility.Public => "public", + _ => "internal", + }; + } + + private static string InferInterfaceName(ISymbol implTypeSymbol, AttributeData attributeData) + { + return attributeData.GetNamedParamValue(Attributes.InterfaceNamePropName) + ?? $"I{implTypeSymbol.Name}"; + } + + private string GenerateInterfaceCode( + INamedTypeSymbol implTypeSymbol, + AttributeData attributeData + ) + { + using var stream = new MemoryStream(); + var streamWriter = new StreamWriter(stream, Encoding.UTF8); + var codeWriter = new IndentedTextWriter(streamWriter, " "); + + var namespaceName = implTypeSymbol.ContainingNamespace.ToDisplayString(); + var interfaceName = InferInterfaceName(implTypeSymbol, attributeData); + var visibilityModifier = InferVisibilityModifier(implTypeSymbol, attributeData); + + //https://stackoverflow.com/questions/55492214/the-annotation-for-nullable-reference-types-should-only-be-used-in-code-within-a fix for nullable + + codeWriter.WriteLine("#nullable enable"); + codeWriter.WriteLine("namespace {0}", namespaceName); + codeWriter.WriteLine("{"); + + ++codeWriter.Indent; + WriteSymbolDocsIfPresent(codeWriter, implTypeSymbol); + codeWriter.Write("{0} partial interface {1}", visibilityModifier, interfaceName); + WriteTypeGenericsIfNeeded(codeWriter, implTypeSymbol); + codeWriter.WriteLine(); + codeWriter.WriteLine("{"); + + ++codeWriter.Indent; + GenerateInterfaceMemberDefinitions(codeWriter, implTypeSymbol); + --codeWriter.Indent; + + codeWriter.WriteLine("}"); + --codeWriter.Indent; + + codeWriter.WriteLine("}"); + codeWriter.WriteLine("#nullable restore"); + + codeWriter.Flush(); + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); + } + + private static void WriteTypeGenericsIfNeeded( + TextWriter writer, + INamedTypeSymbol implTypeSymbol + ) + { + if (!implTypeSymbol.IsGenericType) + { + return; + } + + writer.Write("<"); + writer.WriteJoin(", ", implTypeSymbol.TypeParameters.Select(x => x.Name)); + writer.Write(">"); + + WriteTypeParameterConstraints(writer, implTypeSymbol.TypeParameters); + } + + private void GenerateInterfaceMemberDefinitions( + TextWriter writer, + INamedTypeSymbol implTypeSymbol + ) + { + foreach (var member in implTypeSymbol.GetMembers()) + { + if ( + member.DeclaredAccessibility != Accessibility.Public + || member.HasAttribute( + _ignoreAttribute ?? throw new NullReferenceException("_ignoreAttribute is null") + ) + ) + { + continue; + } + + GenerateInterfaceMemberDefinition(writer, implTypeSymbol, member); + } + } + + private static void GenerateInterfaceMemberDefinition( + TextWriter writer, + INamedTypeSymbol owner, + ISymbol member + ) + { + switch (member) + { + case IPropertySymbol propertySymbol: + GeneratePropertyDefinition(writer, propertySymbol); + break; + case IMethodSymbol methodSymbol: + GenerateMethodDefinition(writer, owner, methodSymbol); + break; + } + } + + private static void WriteSymbolDocsIfPresent(TextWriter writer, ISymbol symbol) + { + var xml = symbol.GetDocumentationCommentXml(); + if (string.IsNullOrWhiteSpace(xml)) + { + return; + } + + // omit the fist and last lines to skip the tag + + var reader = new StringReader(xml); + var lines = new List(); + + while (true) + { + var line = reader.ReadLine(); + if (line is null) + { + break; + } + + lines.Add(line); + } + + for (int i = 1; i < lines.Count - 1; i++) + { + var line = lines[i].TrimStart(); // for some reason, 4 spaces are inserted to the beginning of the line + writer.WriteLine("/// {0}", line); + } + } + + private static bool IsPublicOrInternal(ISymbol symbol) + { + return symbol.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal; + } + + private static void GeneratePropertyDefinition( + TextWriter writer, + IPropertySymbol propertySymbol + ) + { + if (propertySymbol.IsStatic) + { + return; + } + + bool hasPublicGetter = + propertySymbol.GetMethod is not null && IsPublicOrInternal(propertySymbol.GetMethod); + + bool hasPublicSetter = + propertySymbol.SetMethod is not null && IsPublicOrInternal(propertySymbol.SetMethod); + + if (!hasPublicGetter && !hasPublicSetter) + { + return; + } + + WriteSymbolDocsIfPresent(writer, propertySymbol); + + if (propertySymbol.IsIndexer) + { + writer.Write("{0} this[", propertySymbol.Type.GetNamespaceAndType()); + writer.WriteJoin(", ", propertySymbol.Parameters, WriteMethodParam); + writer.Write("] "); + } + else + { + writer.Write("{0} {1} ", propertySymbol.Type, propertySymbol.Name); // ex. int Foo + } + + writer.Write("{ "); + + if (hasPublicGetter) + { + writer.Write("get; "); + } + + if (hasPublicSetter) + { + if (propertySymbol.SetMethod?.IsInitOnly ?? false) + { + writer.Write("init; "); + } + else + { + writer.Write("set; "); + } + } + + writer.WriteLine("}"); + } + + private static void GenerateMethodDefinition( + TextWriter writer, + INamedTypeSymbol owner, + IMethodSymbol methodSymbol + ) + { + if (methodSymbol.MethodKind != MethodKind.Ordinary || methodSymbol.IsStatic) + { + return; + } + + if (methodSymbol.IsImplicitlyDeclared && methodSymbol.Name != "Deconstruct") + { + // omit methods that are auto generated by the compiler (eg. record's methods), + // except for the record Deconstruct method + return; + } + + WriteSymbolDocsIfPresent(writer, methodSymbol); + + writer.Write("{0} {1}", methodSymbol.ReturnType.GetNamespaceAndType(), methodSymbol.Name); // ex. int Foo + + if (methodSymbol.IsGenericMethod) + { + writer.Write("<"); + writer.WriteJoin( + ", ", + methodSymbol.TypeParameters.Select(x => x.GetNamespaceAndType()) + ); + writer.Write(">"); + } + + writer.Write("("); + writer.WriteJoin(", ", methodSymbol.Parameters, WriteMethodParam); + + writer.Write(")"); + + if (methodSymbol.IsGenericMethod) + { + WriteTypeParameterConstraints(writer, methodSymbol.TypeParameters); + } + + writer.WriteLine(";"); + } + + private static void WriteMethodParam(TextWriter writer, IParameterSymbol param) + { + if (param.IsParams) + { + writer.Write("params "); + } + + switch (param.RefKind) + { + case RefKind.Ref: + writer.Write("ref "); + break; + case RefKind.Out: + writer.Write("out "); + break; + case RefKind.In: + writer.Write("in "); + break; + } + + writer.Write(param.Type.GetNamespaceAndType()); + writer.Write(" "); + + if (StringExtensions.IsCSharpKeyword(param.Name)) + { + writer.Write("@"); + } + + writer.Write(param.Name); + + if (param.HasExplicitDefaultValue) + { + WriteParamExplicitDefaultValue(writer, param); + } + } + + private static void WriteParamExplicitDefaultValue(TextWriter writer, IParameterSymbol param) + { + if (param.ExplicitDefaultValue is null) + { + writer.Write(" = default"); + } + else + { + switch (param.Type.Name) + { + case nameof(String): + writer.Write(" = \"{0}\"", param.ExplicitDefaultValue); + break; + case nameof(Single): + writer.Write(" = {0}f", param.ExplicitDefaultValue); + break; + case nameof(Double): + writer.Write(" = {0}d", param.ExplicitDefaultValue); + break; + case nameof(Decimal): + writer.Write(" = {0}m", param.ExplicitDefaultValue); + break; + case nameof(Boolean): + writer.Write(" = {0}", param.ExplicitDefaultValue.ToString().ToLower()); + break; + case nameof(Nullable): + writer.Write(" = {0}", param.ExplicitDefaultValue.ToString().ToLower()); + break; + default: + writer.Write(" = {0}", param.ExplicitDefaultValue); + break; + } + } + } + + private static void WriteTypeParameterConstraints( + TextWriter writer, + IEnumerable typeParameters + ) + { + foreach (var typeParameter in typeParameters) + { + var constraints = typeParameter.EnumGenericConstraints().ToList(); + if (constraints.Count == 0) + { + break; + } + + writer.Write(" where {0} : ", typeParameter.Name); + writer.WriteJoin(", ", constraints); + } + } + + private void InitAttributes(Compilation compilation) + { + _generateAutoInterfaceAttribute = compilation.GetTypeByMetadataName( + $"{Attributes.AttributesNamespace}.{Attributes.GenerateAutoInterfaceClassname}" + ); + + _ignoreAttribute = compilation.GetTypeByMetadataName( + $"{Attributes.AttributesNamespace}.{Attributes.AutoInterfaceIgnoreAttributeClassname}" + ); + } + + private static IEnumerable GetImplTypeSymbols( + Compilation compilation, + SyntaxReceiver receiver + ) + { + return receiver + .CandidateTypes.Select(candidate => GetTypeSymbol(compilation, candidate)) + .Where(x => x != null) + .Cast(); + } + + private static INamedTypeSymbol? GetTypeSymbol(Compilation compilation, SyntaxNode type) + { + var model = compilation.GetSemanticModel(type.SyntaxTree); + var typeSymbol = model.GetDeclaredSymbol(type); + return typeSymbol as INamedTypeSymbol; + } + + private static Compilation GetCompilation(GeneratorExecutionContext context) + { + var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions; + + var compilation = context.Compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText( + SourceText.From(Attributes.AttributesSourceCode, Encoding.UTF8), + options + ) + ); + + return compilation; + } +} diff --git a/Speckle.InterfaceGenerator/Speckle.InterfaceGenerator.csproj b/Speckle.InterfaceGenerator/Speckle.InterfaceGenerator.csproj new file mode 100644 index 0000000..9577c28 --- /dev/null +++ b/Speckle.InterfaceGenerator/Speckle.InterfaceGenerator.csproj @@ -0,0 +1,39 @@ + + + netstandard2.0 + Latest + enable + 0.9.8 + true + false + false + true + + Speckle.InterfaceGenerator + true + true + + + + Speckle.InterfaceGenerator + A source generator that creates interfaces from implementations + https://github.com/specklesystems/InterfaceGenerator + https://github.com/specklesystems/InterfaceGenerator/blob/master/LICENSE + https://github.com/specklesystems/InterfaceGenerator + git + readme.md + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Speckle.InterfaceGenerator/StringExtensions.cs b/Speckle.InterfaceGenerator/StringExtensions.cs new file mode 100644 index 0000000..02a2522 --- /dev/null +++ b/Speckle.InterfaceGenerator/StringExtensions.cs @@ -0,0 +1,124 @@ +namespace Speckle.InterfaceGenerator; + +internal static class StringExtensions +{ + public static bool IsCSharpKeyword(string? name) + { + switch (name) + { + case "abstract": + case "add": + case "alias": + case "as": + case "ascending": + case "async": + case "await": + case "base": + case "bool": + case "break": + case "by": + case "byte": + case "case": + case "catch": + case "char": + case "checked": + case "class": + case "const": + case "continue": + case "decimal": + case "default": + case "delegate": + case "descending": + case "do": + case "double": + case "dynamic": + case "else": + case "enum": + case "equals": + case "event": + case "explicit": + case "extern": + case "false": + case "finally": + case "fixed": + case "float": + case "for": + case "foreach": + case "from": + case "get": + case "global": + case "goto": + // `group` is a contextual to linq queries that we don't generate + //case "group": + case "if": + case "implicit": + case "in": + case "int": + case "interface": + case "internal": + case "into": + case "is": + case "join": + case "let": + case "lock": + case "long": + case "nameof": + case "namespace": + case "new": + case "null": + case "object": + case "on": + case "operator": + // `orderby` is a contextual to linq queries that we don't generate + //case "orderby": + case "out": + case "override": + case "params": + case "partial": + case "private": + case "protected": + case "public": + case "readonly": + case "ref": + case "remove": + case "return": + case "sbyte": + case "sealed": + // `select` is a contextual to linq queries that we don't generate + // case "select": + case "set": + case "short": + case "sizeof": + case "stackalloc": + case "static": + case "string": + case "struct": + case "switch": + case "this": + case "throw": + case "true": + case "try": + case "typeof": + case "uint": + case "ulong": + case "unchecked": + case "unmanaged": + case "unsafe": + case "ushort": + case "using": + // `value` is a contextual to getters that we don't generate + // case "value": + case "var": + case "virtual": + case "void": + case "volatile": + case "when": + case "where": + case "while": + case "yield": + return true; + default: + return false; + } + } +} diff --git a/Speckle.InterfaceGenerator/SymbolExtensions.cs b/Speckle.InterfaceGenerator/SymbolExtensions.cs new file mode 100644 index 0000000..dc733ea --- /dev/null +++ b/Speckle.InterfaceGenerator/SymbolExtensions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Speckle.InterfaceGenerator; + +internal static class SymbolExtensions +{ + public static string GetNamespaceAndType(this ITypeSymbol typeSymbol) + { + if (typeSymbol is ITypeParameterSymbol t) + { + return t.Name; + } + return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + public static bool TryGetAttribute( + this ISymbol symbol, + INamedTypeSymbol attributeType, + out IEnumerable attributes + ) + { + attributes = symbol + .GetAttributes() + .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + return attributes.Any(); + } + + public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) + { + return symbol + .GetAttributes() + .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + } + + //Ref: https://stackoverflow.com/questions/27105909/get-fully-qualified-metadata-name-in-roslyn + public static string GetFullMetadataName(this ISymbol symbol, bool useNameWhenNotFound = false) + { + if (IsRootNamespace(symbol)) + { + return useNameWhenNotFound ? symbol.Name : string.Empty; + } + + var stringBuilder = new StringBuilder(symbol.MetadataName); + var last = symbol; + + symbol = symbol.ContainingSymbol; + + while (!IsRootNamespace(symbol)) + { + if (symbol is ITypeSymbol && last is ITypeSymbol) + { + stringBuilder.Insert(0, '+'); + } + else + { + stringBuilder.Insert(0, '.'); + } + + stringBuilder.Insert( + 0, + symbol.OriginalDefinition.ToDisplayString( + SymbolDisplayFormat.MinimallyQualifiedFormat + ) + ); + symbol = symbol.ContainingSymbol; + } + + var retVal = stringBuilder.ToString(); + if (string.IsNullOrWhiteSpace(retVal) && useNameWhenNotFound) + { + return symbol.Name; + } + + return retVal; + } + + private static bool IsRootNamespace(ISymbol symbol) + { + return symbol is INamespaceSymbol { IsGlobalNamespace: true }; + } +} diff --git a/Speckle.InterfaceGenerator/SyntaxReceiver.cs b/Speckle.InterfaceGenerator/SyntaxReceiver.cs new file mode 100644 index 0000000..7651dee --- /dev/null +++ b/Speckle.InterfaceGenerator/SyntaxReceiver.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Speckle.InterfaceGenerator; + +internal class SyntaxReceiver : ISyntaxReceiver +{ + public IList CandidateTypes { get; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if ( + syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax + && IsClassOrRecord(typeDeclarationSyntax) + && typeDeclarationSyntax.AttributeLists.Count > 0 + ) + { + CandidateTypes.Add(typeDeclarationSyntax); + } + } + + private static bool IsClassOrRecord(TypeDeclarationSyntax typeDeclarationSyntax) + { + return typeDeclarationSyntax is ClassDeclarationSyntax + || typeDeclarationSyntax is RecordDeclarationSyntax; + } +} diff --git a/Speckle.InterfaceGenerator/TextWriterExtensions.cs b/Speckle.InterfaceGenerator/TextWriterExtensions.cs new file mode 100644 index 0000000..c2c5d94 --- /dev/null +++ b/Speckle.InterfaceGenerator/TextWriterExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Speckle.InterfaceGenerator; + +internal static class TextWriterExtensions +{ + public static void WriteJoin(this TextWriter writer, string separator, IEnumerable values) + { + writer.WriteJoin(separator, values, (w, x) => w.Write(x)); + } + + public static void WriteJoin( + this TextWriter writer, + string separator, + IEnumerable values, + Action writeAction + ) + { + using var enumerator = values.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + return; + } + + writeAction(writer, enumerator.Current); + + if (!enumerator.MoveNext()) + { + return; + } + + do + { + writer.Write(separator); + writeAction(writer, enumerator.Current); + } while (enumerator.MoveNext()); + } +} diff --git a/Speckle.InterfaceGenerator/TypeParameterSymbolExtensions.cs b/Speckle.InterfaceGenerator/TypeParameterSymbolExtensions.cs new file mode 100644 index 0000000..d33a8ba --- /dev/null +++ b/Speckle.InterfaceGenerator/TypeParameterSymbolExtensions.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Speckle.InterfaceGenerator; + +internal static class TypeParameterSymbolExtensions +{ + public static IEnumerable EnumGenericConstraints(this ITypeParameterSymbol symbol) + { + // the class/struct/unmanaged/notnull constraint has to be the last + if (symbol.HasNotNullConstraint) + { + yield return "notnull"; + } + + if (symbol.HasValueTypeConstraint) + { + yield return "struct"; + } + + if (symbol.HasUnmanagedTypeConstraint) + { + yield return "unmanaged"; + } + + if (symbol.HasReferenceTypeConstraint) + { + yield return symbol.ReferenceTypeConstraintNullableAnnotation + == NullableAnnotation.Annotated + ? "class?" + : "class"; + } + + // types go in the middle + foreach (var constraintType in symbol.ConstraintTypes) + { + yield return constraintType.ToDisplayString(); + } + + // the new() constraint has to be the last + if (symbol.HasConstructorConstraint) + { + yield return "new()"; + } + } +} diff --git a/global.json b/global.json new file mode 100644 index 0000000..18b689d --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +}