diff --git a/.editorconfig b/.editorconfig index f5922969..01cccffd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,71 +3,26 @@ # top-most EditorConfig file root = true -# Default settings: -# A newline ending every file -# Use 4 spaces as indentation +############################################# +# Default settings +############################################# [*] +end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 -dotnet_diagnostic.CA1027.severity=error -dotnet_diagnostic.CA1062.severity=error -dotnet_diagnostic.CA1064.severity=error -dotnet_diagnostic.CA1066.severity=error -dotnet_diagnostic.CA1067.severity=error -dotnet_diagnostic.CA1068.severity=error -dotnet_diagnostic.CA1069.severity=warning -dotnet_diagnostic.CA2013.severity=error -dotnet_diagnostic.CA1802.severity=error -dotnet_diagnostic.CA1813.severity=error -dotnet_diagnostic.CA1814.severity=error -dotnet_diagnostic.CA1815.severity=error -dotnet_diagnostic.CA1822.severity=error -dotnet_diagnostic.CA1827.severity=error -dotnet_diagnostic.CA1828.severity=error -dotnet_diagnostic.CA1826.severity=error -dotnet_diagnostic.CA1829.severity=error -dotnet_diagnostic.CA1830.severity=error -dotnet_diagnostic.CA1831.severity=error -dotnet_diagnostic.CA1832.severity=error -dotnet_diagnostic.CA1833.severity=error -dotnet_diagnostic.CA1834.severity=error -dotnet_diagnostic.CA1835.severity=error -dotnet_diagnostic.CA1836.severity=error -dotnet_diagnostic.CA1837.severity=error -dotnet_diagnostic.CA1838.severity=error -dotnet_diagnostic.CA2015.severity=error -dotnet_diagnostic.CA2012.severity=error -dotnet_diagnostic.CA2011.severity=error -dotnet_diagnostic.CA2009.severity=error -dotnet_diagnostic.CA2008.severity=error -dotnet_diagnostic.CA2007.severity=warning -dotnet_diagnostic.CA2000.severity=suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -end_of_line = lf -dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion [project.json] indent_size = 2 -# C# files +############################################# +# C# Code Style Settings +############################################# [*.cs] + +################### # New line preferences +################### csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true @@ -76,7 +31,9 @@ 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 preferences +################### csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true @@ -84,80 +41,96 @@ csharp_indent_case_contents_when_block = true csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current +################### # Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +################### +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion -# avoid this. unless absolutely necessary +################### +# 'this.' qualification +################### dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion -# only use var when it's obvious what the variable type is +################### +# 'var' preferences +################### csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = true:suggestion -# prefer C# premade types. +################### +# Predefined type preferences +################### dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion -# name all constant fields using PascalCase +################### +# Naming conventions +################### + +# Constant fields should be PascalCase dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +# Local constants should be PascalCase +dotnet_naming_rule.local_constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_constants_should_be_pascal_case.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +# Static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = none +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.applicable_kinds = field dotnet_naming_symbols.static_fields.required_modifiers = static dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected dotnet_naming_style.static_prefix_style.required_prefix = s_ dotnet_naming_style.static_prefix_style.capitalization = camel_case -# internal and private fields should be _camelCase +# Internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal dotnet_naming_style.camel_case_underscore_style.required_prefix = _ dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +################### # Code style defaults +################### csharp_using_directive_placement = outside_namespace:suggestion dotnet_sort_system_directives_first = true csharp_prefer_braces = true:silent csharp_preserve_single_line_blocks = true:none csharp_preserve_single_line_statements = false:none -csharp_prefer_static_local_function = true:suggestion -csharp_prefer_simple_using_statement = false:none -csharp_style_prefer_switch_expression = true:suggestion +################### # Code quality -dotnet_style_readonly_field = true:suggestion +################### dotnet_code_quality_unused_parameters = non_public:suggestion +################### # Expression-level preferences -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion +################### dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_auto_properties = true:suggestion dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent -csharp_prefer_simple_default_expression = true:suggestion +################### # Expression-bodied members +################### csharp_style_expression_bodied_methods = true:suggestion csharp_style_expression_bodied_constructors = true:suggestion csharp_style_expression_bodied_operators = true:suggestion @@ -167,21 +140,16 @@ csharp_style_expression_bodied_accessors = true:suggestion csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_local_functions = true:suggestion -# Pattern matching -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - -# Null checking preferences -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion - +################### # Other features +################### csharp_style_prefer_index_operator = false:none csharp_style_prefer_range_operator = false:none csharp_style_pattern_local_over_anonymous_function = false:none +################### # Space preferences +################### csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true @@ -205,463 +173,2666 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false -# analyzers +############################################# +# Code Analyzers +############################################# + +################### +# Custom Analyzers +################### dotnet_diagnostic.AvoidAsyncVoid.severity = suggestion -dotnet_diagnostic.CA1000.severity = none -dotnet_diagnostic.CA1001.severity = error -dotnet_diagnostic.CA1009.severity = error -dotnet_diagnostic.CA1016.severity = error -dotnet_diagnostic.CA1030.severity = none -dotnet_diagnostic.CA1031.severity = none -dotnet_diagnostic.CA1033.severity = none -dotnet_diagnostic.CA1036.severity = none -dotnet_diagnostic.CA1049.severity = error -dotnet_diagnostic.CA1056.severity = suggestion -dotnet_diagnostic.CA1060.severity = error -dotnet_diagnostic.CA1061.severity = error -dotnet_diagnostic.CA1063.severity = error -dotnet_diagnostic.CA1065.severity = error -dotnet_diagnostic.CA1301.severity = error -dotnet_diagnostic.CA1303.severity = none -dotnet_diagnostic.CA1308.severity = none -dotnet_diagnostic.CA1400.severity = error -dotnet_diagnostic.CA1401.severity = error -dotnet_diagnostic.CA1403.severity = error -dotnet_diagnostic.CA1404.severity = error -dotnet_diagnostic.CA1405.severity = error -dotnet_diagnostic.CA1410.severity = error -dotnet_diagnostic.CA1415.severity = error -dotnet_diagnostic.CA1507.severity = error -dotnet_diagnostic.CA1710.severity = suggestion -dotnet_diagnostic.CA1724.severity = none -dotnet_diagnostic.CA1810.severity = none -dotnet_diagnostic.CA1821.severity = error -dotnet_diagnostic.CA1900.severity = error -dotnet_diagnostic.CA1901.severity = error -dotnet_diagnostic.CA2000.severity = none -dotnet_diagnostic.CA2002.severity = error -dotnet_diagnostic.CA2007.severity = none -dotnet_diagnostic.CA2100.severity = error -dotnet_diagnostic.CA2101.severity = error -dotnet_diagnostic.CA2108.severity = error -dotnet_diagnostic.CA2111.severity = error -dotnet_diagnostic.CA2112.severity = error -dotnet_diagnostic.CA2114.severity = error -dotnet_diagnostic.CA2116.severity = error -dotnet_diagnostic.CA2117.severity = error -dotnet_diagnostic.CA2122.severity = error -dotnet_diagnostic.CA2123.severity = error -dotnet_diagnostic.CA2124.severity = error -dotnet_diagnostic.CA2126.severity = error -dotnet_diagnostic.CA2131.severity = error -dotnet_diagnostic.CA2132.severity = error -dotnet_diagnostic.CA2133.severity = error -dotnet_diagnostic.CA2134.severity = error -dotnet_diagnostic.CA2137.severity = error -dotnet_diagnostic.CA2138.severity = error -dotnet_diagnostic.CA2140.severity = error -dotnet_diagnostic.CA2141.severity = error -dotnet_diagnostic.CA2146.severity = error -dotnet_diagnostic.CA2147.severity = error -dotnet_diagnostic.CA2149.severity = error -dotnet_diagnostic.CA2200.severity = error -dotnet_diagnostic.CA2202.severity = error -dotnet_diagnostic.CA2207.severity = error -dotnet_diagnostic.CA2212.severity = error -dotnet_diagnostic.CA2213.severity = error -dotnet_diagnostic.CA2214.severity = error -dotnet_diagnostic.CA2216.severity = error -dotnet_diagnostic.CA2220.severity = error -dotnet_diagnostic.CA2229.severity = error -dotnet_diagnostic.CA2231.severity = error -dotnet_diagnostic.CA2232.severity = error -dotnet_diagnostic.CA2235.severity = error -dotnet_diagnostic.CA2236.severity = error -dotnet_diagnostic.CA2237.severity = error -dotnet_diagnostic.CA2238.severity = error -dotnet_diagnostic.CA2240.severity = error -dotnet_diagnostic.CA2241.severity = error -dotnet_diagnostic.CA2242.severity = error - -dotnet_diagnostic.RCS1001.severity = error -dotnet_diagnostic.RCS1018.severity = error -dotnet_diagnostic.RCS1037.severity = error -dotnet_diagnostic.RCS1055.severity = error -dotnet_diagnostic.RCS1062.severity = error -dotnet_diagnostic.RCS1066.severity = error -dotnet_diagnostic.RCS1069.severity = error -dotnet_diagnostic.RCS1071.severity = error -dotnet_diagnostic.RCS1074.severity = error -dotnet_diagnostic.RCS1090.severity = error -dotnet_diagnostic.RCS1138.severity = error -dotnet_diagnostic.RCS1139.severity = error -dotnet_diagnostic.RCS1163.severity = suggestion -dotnet_diagnostic.RCS1168.severity = suggestion -dotnet_diagnostic.RCS1188.severity = error -dotnet_diagnostic.RCS1201.severity = error -dotnet_diagnostic.RCS1207.severity = error -dotnet_diagnostic.RCS1211.severity = error -dotnet_diagnostic.RCS1507.severity = error - -dotnet_diagnostic.SA1000.severity = error -dotnet_diagnostic.SA1001.severity = error -dotnet_diagnostic.SA1002.severity = error -dotnet_diagnostic.SA1003.severity = error -dotnet_diagnostic.SA1004.severity = error -dotnet_diagnostic.SA1005.severity = error -dotnet_diagnostic.SA1006.severity = error -dotnet_diagnostic.SA1007.severity = error -dotnet_diagnostic.SA1008.severity = error -dotnet_diagnostic.SA1009.severity = error -dotnet_diagnostic.SA1010.severity = none -dotnet_diagnostic.SA1011.severity = error -dotnet_diagnostic.SA1012.severity = error -dotnet_diagnostic.SA1013.severity = error -dotnet_diagnostic.SA1014.severity = error -dotnet_diagnostic.SA1015.severity = error -dotnet_diagnostic.SA1016.severity = error -dotnet_diagnostic.SA1017.severity = error -dotnet_diagnostic.SA1018.severity = error -dotnet_diagnostic.SA1019.severity = error -dotnet_diagnostic.SA1020.severity = error -dotnet_diagnostic.SA1021.severity = error -dotnet_diagnostic.SA1022.severity = error -dotnet_diagnostic.SA1023.severity = error -dotnet_diagnostic.SA1024.severity = error -dotnet_diagnostic.SA1025.severity = error -dotnet_diagnostic.SA1026.severity = error -dotnet_diagnostic.SA1027.severity = error -dotnet_diagnostic.SA1028.severity = error -dotnet_diagnostic.SA1100.severity = error -dotnet_diagnostic.SA1101.severity = none -dotnet_diagnostic.SA1102.severity = error -dotnet_diagnostic.SA1103.severity = error -dotnet_diagnostic.SA1104.severity = error -dotnet_diagnostic.SA1105.severity = error -dotnet_diagnostic.SA1106.severity = error -dotnet_diagnostic.SA1107.severity = error -dotnet_diagnostic.SA1108.severity = error -dotnet_diagnostic.SA1110.severity = error -dotnet_diagnostic.SA1111.severity = error -dotnet_diagnostic.SA1112.severity = error -dotnet_diagnostic.SA1113.severity = error -dotnet_diagnostic.SA1114.severity = error -dotnet_diagnostic.SA1115.severity = error -dotnet_diagnostic.SA1116.severity = error -dotnet_diagnostic.SA1117.severity = error -dotnet_diagnostic.SA1118.severity = error -dotnet_diagnostic.SA1119.severity = error -dotnet_diagnostic.SA1120.severity = error -dotnet_diagnostic.SA1121.severity = error -dotnet_diagnostic.SA1122.severity = error -dotnet_diagnostic.SA1123.severity = error -dotnet_diagnostic.SA1124.severity = error -dotnet_diagnostic.SA1125.severity = error -dotnet_diagnostic.SA1127.severity = error -dotnet_diagnostic.SA1128.severity = error -dotnet_diagnostic.SA1129.severity = error -dotnet_diagnostic.SA1130.severity = error -dotnet_diagnostic.SA1131.severity = error -dotnet_diagnostic.SA1132.severity = error -dotnet_diagnostic.SA1133.severity = error -dotnet_diagnostic.SA1134.severity = error -dotnet_diagnostic.SA1135.severity = error -dotnet_diagnostic.SA1136.severity = error -dotnet_diagnostic.SA1137.severity = error -dotnet_diagnostic.SA1139.severity = error -dotnet_diagnostic.SA1200.severity = none -dotnet_diagnostic.SA1201.severity = error -dotnet_diagnostic.SA1202.severity = error -dotnet_diagnostic.SA1203.severity = error -dotnet_diagnostic.SA1204.severity = error -dotnet_diagnostic.SA1205.severity = error -dotnet_diagnostic.SA1206.severity = error -dotnet_diagnostic.SA1207.severity = error -dotnet_diagnostic.SA1208.severity = error -dotnet_diagnostic.SA1209.severity = error -dotnet_diagnostic.SA1210.severity = error -dotnet_diagnostic.SA1211.severity = error -dotnet_diagnostic.SA1212.severity = error -dotnet_diagnostic.SA1213.severity = error -dotnet_diagnostic.SA1214.severity = error -dotnet_diagnostic.SA1216.severity = error -dotnet_diagnostic.SA1217.severity = error -dotnet_diagnostic.SA1300.severity = error -dotnet_diagnostic.SA1302.severity = error -dotnet_diagnostic.SA1303.severity = error -dotnet_diagnostic.SA1304.severity = error -dotnet_diagnostic.SA1306.severity = none -dotnet_diagnostic.SA1307.severity = error -dotnet_diagnostic.SA1308.severity = error -dotnet_diagnostic.SA1309.severity = none -dotnet_diagnostic.SA1310.severity = error -dotnet_diagnostic.SA1311.severity = none -dotnet_diagnostic.SA1312.severity = error -dotnet_diagnostic.SA1313.severity = error -dotnet_diagnostic.SA1314.severity = error -dotnet_diagnostic.SA1316.severity = none -dotnet_diagnostic.SA1400.severity = error -dotnet_diagnostic.SA1401.severity = error -dotnet_diagnostic.SA1402.severity = error -dotnet_diagnostic.SA1403.severity = error -dotnet_diagnostic.SA1404.severity = error -dotnet_diagnostic.SA1405.severity = error -dotnet_diagnostic.SA1406.severity = error -dotnet_diagnostic.SA1407.severity = error -dotnet_diagnostic.SA1408.severity = error -dotnet_diagnostic.SA1410.severity = error -dotnet_diagnostic.SA1411.severity = error -dotnet_diagnostic.SA1413.severity = none -dotnet_diagnostic.SA1500.severity = error -dotnet_diagnostic.SA1501.severity = error -dotnet_diagnostic.SA1502.severity = error -dotnet_diagnostic.SA1503.severity = error -dotnet_diagnostic.SA1504.severity = error -dotnet_diagnostic.SA1505.severity = none -dotnet_diagnostic.SA1506.severity = error -dotnet_diagnostic.SA1507.severity = error -dotnet_diagnostic.SA1508.severity = error -dotnet_diagnostic.SA1509.severity = error -dotnet_diagnostic.SA1510.severity = error -dotnet_diagnostic.SA1511.severity = error -dotnet_diagnostic.SA1512.severity = error -dotnet_diagnostic.SA1513.severity = error -dotnet_diagnostic.SA1514.severity = none -dotnet_diagnostic.SA1515.severity = error -dotnet_diagnostic.SA1516.severity = error -dotnet_diagnostic.SA1517.severity = error -dotnet_diagnostic.SA1518.severity = error -dotnet_diagnostic.SA1519.severity = error -dotnet_diagnostic.SA1520.severity = error -dotnet_diagnostic.SA1600.severity = error -dotnet_diagnostic.SA1601.severity = error -dotnet_diagnostic.SA1602.severity = error -dotnet_diagnostic.SA1604.severity = error -dotnet_diagnostic.SA1605.severity = error -dotnet_diagnostic.SA1606.severity = error -dotnet_diagnostic.SA1607.severity = error -dotnet_diagnostic.SA1608.severity = error -dotnet_diagnostic.SA1610.severity = error -dotnet_diagnostic.SA1611.severity = error -dotnet_diagnostic.SA1612.severity = error -dotnet_diagnostic.SA1613.severity = error -dotnet_diagnostic.SA1614.severity = error -dotnet_diagnostic.SA1615.severity = error -dotnet_diagnostic.SA1616.severity = error -dotnet_diagnostic.SA1617.severity = error -dotnet_diagnostic.SA1618.severity = error -dotnet_diagnostic.SA1619.severity = error -dotnet_diagnostic.SA1620.severity = error -dotnet_diagnostic.SA1621.severity = error -dotnet_diagnostic.SA1622.severity = error -dotnet_diagnostic.SA1623.severity = error -dotnet_diagnostic.SA1624.severity = error -dotnet_diagnostic.SA1625.severity = error -dotnet_diagnostic.SA1626.severity = error -dotnet_diagnostic.SA1627.severity = error -dotnet_diagnostic.SA1629.severity = error -dotnet_diagnostic.SA1633.severity = error -dotnet_diagnostic.SA1634.severity = error -dotnet_diagnostic.SA1635.severity = error -dotnet_diagnostic.SA1636.severity = error -dotnet_diagnostic.SA1637.severity = none -dotnet_diagnostic.SA1638.severity = none -dotnet_diagnostic.SA1640.severity = error -dotnet_diagnostic.SA1641.severity = error -dotnet_diagnostic.SA1642.severity = error -dotnet_diagnostic.SA1643.severity = error -dotnet_diagnostic.SA1649.severity = error -dotnet_diagnostic.SA1651.severity = error - -dotnet_diagnostic.SX1101.severity = error -dotnet_diagnostic.SX1309.severity = error -dotnet_diagnostic.SX1623.severity = none -dotnet_diagnostic.RCS1102.severity=error -dotnet_diagnostic.RCS1166.severity=error -dotnet_diagnostic.RCS1078i.severity=error -dotnet_diagnostic.RCS1248.severity=error -dotnet_diagnostic.RCS1080.severity=error -dotnet_diagnostic.RCS1077.severity=error -dotnet_diagnostic.CA1825.severity=error -dotnet_diagnostic.CA1812.severity=error -dotnet_diagnostic.CA1805.severity=error -dotnet_diagnostic.RCS1197.severity=error -dotnet_diagnostic.RCS1198.severity=none -dotnet_diagnostic.RCS1231.severity=suggestion -dotnet_diagnostic.RCS1235.severity=error -dotnet_diagnostic.RCS1242.severity=error -dotnet_diagnostic.RCS1256.severity=none -dotnet_diagnostic.CA2016.severity=warning -dotnet_diagnostic.CA2014.severity=error -dotnet_diagnostic.RCS1010.severity=error -dotnet_diagnostic.RCS1006.severity=error -dotnet_diagnostic.RCS1005.severity=error -dotnet_diagnostic.RCS1020.severity=error -dotnet_diagnostic.RCS1049.severity=warning -dotnet_diagnostic.RCS1058.severity=warning -dotnet_diagnostic.RCS1068.severity=warning -dotnet_diagnostic.RCS1073.severity=warning -dotnet_diagnostic.RCS1084.severity=error -dotnet_diagnostic.RCS1085.severity=error -dotnet_diagnostic.RCS1105.severity=error -dotnet_diagnostic.RCS1112.severity=error -dotnet_diagnostic.RCS1128.severity=error -dotnet_diagnostic.RCS1143.severity=error -dotnet_diagnostic.RCS1158.severity=none -dotnet_diagnostic.RCS1163.severity=none -dotnet_diagnostic.RCS1171.severity=error -dotnet_diagnostic.RCS1173.severity=error -dotnet_diagnostic.RCS1176.severity=error -dotnet_diagnostic.RCS1177.severity=error -dotnet_diagnostic.RCS1179.severity=error -dotnet_diagnostic.RCS1180.severity=warning -dotnet_diagnostic.RCS1190.severity=error -dotnet_diagnostic.RCS1195.severity=error -dotnet_diagnostic.RCS1214.severity=error -csharp_style_namespace_declarations = file_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_primary_constructors = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion +################### +# Microsoft.CodeAnalysis.NetAnalyzers (CA) +################### +# Design +dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types — common factory pattern +dotnet_diagnostic.CA1001.severity = error # Types that own disposable fields should be disposable +dotnet_diagnostic.CA1002.severity = none # Do not expose generic lists — we deliberately expose List; interface-based collections are an older convention we don't follow +dotnet_diagnostic.CA1003.severity = none # Use generic event handler instances — covered by SST2304 +dotnet_diagnostic.CA1005.severity = none # Avoid excessive parameters on generic types — we deliberately expose 3+ type-parameter types (tuple-style handles, raw engine signals); the ergonomic guidance conflicts with that design +dotnet_diagnostic.CA1008.severity = error # Enums should have zero value +dotnet_diagnostic.CA1010.severity = none # Collections should implement generic interface — we deliberately expose concrete collection types; interface-based collections are an older convention we don't follow +dotnet_diagnostic.CA1012.severity = none # Abstract types should not have public constructors — covered by SST1428 +dotnet_diagnostic.CA1014.severity = none # Mark assemblies with CLSCompliantAttribute — we don't ship CLS-compliant assemblies +dotnet_diagnostic.CA1016.severity = error # Mark assemblies with AssemblyVersionAttribute +dotnet_diagnostic.CA1017.severity = none # Mark assemblies with ComVisibleAttribute — we don't ship COM-visible assemblies +dotnet_diagnostic.CA1018.severity = error # Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1019.severity = none # Define accessors for attribute arguments — conflicts with SST2324, which caps an internal attribute's accessor at internal +dotnet_diagnostic.CA1021.severity = none # Avoid out parameters - disabled - needed for the zero-allocation idiom in Try/Find APIs and other performance-critical paths +dotnet_diagnostic.CA1024.severity = error # Use properties where appropriate +dotnet_diagnostic.CA1027.severity = error # Mark enums with FlagsAttribute +dotnet_diagnostic.CA1028.severity = none # Enum storage should be Int32 — covered by SST2313 +dotnet_diagnostic.CA1030.severity = none # Use events where appropriate — we use Rx observables instead of CLR events +dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types — required at logging/dispose/IO boundaries +dotnet_diagnostic.CA1032.severity = none # Implement standard exception constructors — covered by SST1488 +dotnet_diagnostic.CA1033.severity = none # Interface methods should be callable by child types — explicit interface implementations are a deliberate design choice +dotnet_diagnostic.CA1034.severity = none # Nested types should not be visible — public nested types are sometimes the cleanest API (e.g. interface-scoped exception helpers) +dotnet_diagnostic.CA1036.severity = none # Override methods on comparable types — relational operators rarely meaningful for our types +dotnet_diagnostic.CA1040.severity = none # Avoid empty interfaces — duplicate of SST1437 (canonical); marker interfaces (IActivatableView etc.) are deliberate public API +dotnet_diagnostic.CA1041.severity = none # Provide ObsoleteAttribute message — covered by SST2308 +dotnet_diagnostic.CA1043.severity = error # Use integral or string argument for indexers +dotnet_diagnostic.CA1044.severity = none # Properties should not be write only — covered by SST1421 +dotnet_diagnostic.CA1045.severity = none # Do not pass types by reference — we deliberately use ref-passing static helpers so they carry only the data they touch; data-oriented layout is the default here +dotnet_diagnostic.CA1046.severity = error # Do not overload operator equals on reference types +dotnet_diagnostic.CA1047.severity = none # Do not declare protected member in sealed type — covered by SST1427 +dotnet_diagnostic.CA1048.severity = none # Do not declare virtual members in sealed types — covered by SST1491 +dotnet_diagnostic.CA1050.severity = none # Declare types in namespaces — covered by SST2312 +dotnet_diagnostic.CA1051.severity = none # Duplicate of SST1401 (canonical) — do not declare visible instance fields +dotnet_diagnostic.CA1052.severity = none # Static holder types should be sealed — covered by SST1432 +dotnet_diagnostic.CA1053.severity = none # Static holder types should not have constructors — covered by SST1432 +dotnet_diagnostic.CA1054.severity = suggestion # URI parameters should not be strings +dotnet_diagnostic.CA1055.severity = suggestion # URI return values should not be strings +dotnet_diagnostic.CA1056.severity = suggestion # URI properties should not be strings +dotnet_diagnostic.CA1058.severity = error # Types should not extend certain base types +dotnet_diagnostic.CA1059.severity = error # Members should not expose certain concrete types +dotnet_diagnostic.CA1060.severity = error # Move P/Invokes to NativeMethods class +dotnet_diagnostic.CA1061.severity = none # Do not hide base class methods — covered by SST2427 +dotnet_diagnostic.CA1062.severity = none # Validate arguments of public methods - Nullable=enable + we own every consumer, so the compiler already guarantees non-null params +dotnet_diagnostic.CA1063.severity = none # Implement IDisposable correctly — covered by SST2300 +dotnet_diagnostic.CA1064.severity = error # Exceptions should be public +dotnet_diagnostic.CA1065.severity = none # Do not raise exceptions in unexpected locations — covered by SST1485 +dotnet_diagnostic.CA1066.severity = error # Implement IEquatable when overriding Equals +dotnet_diagnostic.CA1067.severity = error # Override Equals when implementing IEquatable +dotnet_diagnostic.CA1068.severity = error # CancellationToken parameters must come last +dotnet_diagnostic.CA1069.severity = error # Enums should not have duplicate values +dotnet_diagnostic.CA1070.severity = error # Do not declare event fields as virtual + +# Globalization +dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters — we don't ship localized resources +dotnet_diagnostic.CA1307.severity = none # Covered by PSH1207 (canonical) +dotnet_diagnostic.CA1308.severity = none # Normalize strings to uppercase — ToLowerInvariant is correct for filesystem path / cache key normalization +dotnet_diagnostic.CA1310.severity = none # Covered by PSH1207 (canonical) + +# Interoperability +dotnet_diagnostic.CA1401.severity = error # P/Invokes should not be visible + +# Maintainability +dotnet_diagnostic.CA1500.severity = none # Variable names should not match field names — covered by SST1484 +dotnet_diagnostic.CA1501.severity = none # Covered by SST1446 (canonical) +dotnet_diagnostic.CA1502.severity = none # Covered by SST1442 (canonical) +dotnet_diagnostic.CA1505.severity = error # Avoid unmaintainable code +dotnet_diagnostic.CA1506.severity = none # Avoid excessive class coupling — adds little signal here, mostly trips on legitimate orchestration code +dotnet_diagnostic.CA1507.severity = none # Use nameof in place of string — covered by SST1463 +dotnet_diagnostic.CA1508.severity = error # Avoid dead conditional code +dotnet_diagnostic.CA1509.severity = error # Invalid entry in code metrics configuration file +dotnet_diagnostic.CA1510.severity = none # Covered by PSH1409 (canonical) +dotnet_diagnostic.CA1511.severity = none # Covered by PSH1409 (canonical) +dotnet_diagnostic.CA1512.severity = none # Covered by PSH1409 (canonical) +dotnet_diagnostic.CA1513.severity = none # Covered by PSH1409 (canonical) +dotnet_diagnostic.CA1514.severity = none # Avoid redundant length argument — covered by PSH1220 +dotnet_diagnostic.CA1515.severity = none # Consider making public types internal — interferes with tests and reflection-discovered types (BenchmarkDotNet, TUnit, etc.) +dotnet_diagnostic.CA1516.severity = error # Use cross-platform intrinsics + +# Naming +dotnet_diagnostic.CA1710.severity = suggestion # Identifiers should have correct suffix +dotnet_diagnostic.CA1724.severity = none # Type Names Should Not Match Namespaces — namespace/type name overlap is intentional API surface + +# Performance +dotnet_diagnostic.CA1802.severity = none # Use literals where appropriate — covered by PSH1402 +dotnet_diagnostic.CA1805.severity = none # Do not initialize unnecessarily — covered by PSH1403 +dotnet_diagnostic.CA1806.severity = error # Do not ignore method results +dotnet_diagnostic.CA1810.severity = none # Initialize reference type static fields inline — explicit static constructors are deliberate in some types +dotnet_diagnostic.CA1812.severity = error # Avoid uninstantiated internal classes +dotnet_diagnostic.CA1813.severity = none # Avoid unsealed attributes — covered by PSH1401 +dotnet_diagnostic.CA1814.severity = none # Prefer jagged arrays over multidimensional — covered by PSH1020 +dotnet_diagnostic.CA1815.severity = none # Override equals and operator equals on value types — covered by PSH1005 +dotnet_diagnostic.CA1819.severity = none # Properties should not return arrays — incompatible with the RxUI/sqlite-net mapping style we use throughout the codebase +dotnet_diagnostic.CA1820.severity = none # Test for empty strings using string length — covered by PSH1204 +dotnet_diagnostic.CA1821.severity = none # Remove empty finalizers — covered by PSH1002 +dotnet_diagnostic.CA1822.severity = none # Mark members as static — covered by PSH1414 +dotnet_diagnostic.CA1823.severity = none # Avoid unused private fields — covered by SST1441 +dotnet_diagnostic.CA1824.severity = error # Mark assemblies with NeutralResourcesLanguageAttribute +dotnet_diagnostic.CA1825.severity = none # Avoid zero-length array allocations — covered by PSH1001 +dotnet_diagnostic.CA1826.severity = none # Use property instead of Linq Enumerable method — covered by PSH1103 +dotnet_diagnostic.CA1827.severity = none # Do not use Count/LongCount when Any can be used — covered by PSH1119 +dotnet_diagnostic.CA1828.severity = none # Do not use CountAsync/LongCountAsync when AnyAsync can be used — covered by PSH1126 +dotnet_diagnostic.CA1829.severity = none # Use Length/Count property instead of Enumerable.Count — covered by PSH1103 +dotnet_diagnostic.CA1830.severity = none # Prefer strongly-typed Append/Insert overloads on StringBuilder — covered by PSH1202 +dotnet_diagnostic.CA1831.severity = none # Use AsSpan instead of Range-based indexers for string — covered by PSH1212 +dotnet_diagnostic.CA1832.severity = none # Use AsSpan or AsMemory instead of Range-based indexers for getting ReadOnlySpan or ReadOnlyMemory portion of an array — covered by PSH1019 +dotnet_diagnostic.CA1833.severity = none # Use AsSpan or AsMemory instead of Range-based indexers for getting Span or Memory portion of an array — conflicts with PSH1019, which owns the array range-indexer rewrite and refuses it for mutable Span/Memory targets +dotnet_diagnostic.CA1834.severity = none # Use StringBuilder.Append(char) for single character strings — covered by PSH1202 +dotnet_diagnostic.CA1835.severity = none # Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes — covered by PSH1314 +dotnet_diagnostic.CA1836.severity = none # Prefer IsEmpty over Count when available — covered by PSH1117 +dotnet_diagnostic.CA1837.severity = none # Use Environment.ProcessId — covered by PSH1405 +dotnet_diagnostic.CA1838.severity = error # Avoid StringBuilder parameters for P/Invokes +dotnet_diagnostic.CA1839.severity = none # Use Environment.ProcessPath — covered by PSH1405 +dotnet_diagnostic.CA1840.severity = none # Use Environment.CurrentManagedThreadId — covered by PSH1405 +dotnet_diagnostic.CA1841.severity = none # Prefer Dictionary.Contains methods — covered by PSH1407 +dotnet_diagnostic.CA1842.severity = none # Do not use 'WhenAll' with a single task — covered by PSH1301 +dotnet_diagnostic.CA1843.severity = none # Do not use 'WaitAll' with a single task — covered by PSH1301 +dotnet_diagnostic.CA1844.severity = error # Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_diagnostic.CA1845.severity = none # Use span-based 'string.Concat' — covered by PSH1222 +dotnet_diagnostic.CA1846.severity = none # Prefer AsSpan over Substring — covered by PSH1212 +dotnet_diagnostic.CA1847.severity = none # Covered by PSH1201 (canonical) +dotnet_diagnostic.CA1848.severity = error # Use the LoggerMessage delegates +dotnet_diagnostic.CA1849.severity = none # Call async methods when in an async method — covered by PSH1313 +dotnet_diagnostic.CA1850.severity = none # Prefer static HashData method over ComputeHash — covered by PSH1400 +dotnet_diagnostic.CA1851.severity = error # Possible multiple enumerations of IEnumerable collection +dotnet_diagnostic.CA1852.severity = none # Seal internal types — covered by PSH1411 +dotnet_code_quality.CA1852.api_surface = private, internal # only flag non-public classes; public classes stay open for inheritance +dotnet_diagnostic.CA1853.severity = none # Unnecessary call to 'Dictionary.ContainsKey(key)' — covered by PSH1105 +dotnet_diagnostic.CA1854.severity = none # Prefer the IDictionary.TryGetValue method — covered by PSH1104 +dotnet_diagnostic.CA1855.severity = error # Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1856.severity = error # Incorrect usage of ConstantExpected attribute +dotnet_diagnostic.CA1857.severity = error # A constant is expected for the parameter +dotnet_diagnostic.CA1858.severity = none # Use 'StartsWith' instead of 'IndexOf' — covered by PSH1221 +dotnet_diagnostic.CA1859.severity = error # Use concrete types when possible for improved performance +dotnet_diagnostic.CA1860.severity = none # Avoid using 'Enumerable.Any()' extension method — covered by PSH1103 +dotnet_diagnostic.CA1861.severity = none # Avoid constant arrays as arguments — covered by PSH1004 +dotnet_diagnostic.CA1862.severity = none # Use the 'StringComparison' overloads for case-insensitive comparisons — covered by PSH1200 +dotnet_diagnostic.CA1863.severity = none # Use 'CompositeFormat' — covered by PSH1223 +dotnet_diagnostic.CA1864.severity = none # Prefer the 'IDictionary.TryAdd' method — covered by PSH1115 +dotnet_diagnostic.CA1865.severity = none # Covered by PSH1201 (canonical) +dotnet_diagnostic.CA1866.severity = none # Covered by PSH1201 (canonical) +dotnet_diagnostic.CA1867.severity = none # Covered by PSH1201 (canonical) +dotnet_diagnostic.CA1868.severity = none # Unnecessary call to 'Contains' for sets — covered by PSH1105 +dotnet_diagnostic.CA1869.severity = none # Cache and reuse 'JsonSerializerOptions' instances — covered by PSH1416 +dotnet_diagnostic.CA1870.severity = none # Use a cached 'SearchValues' instance — covered by PSH1213 +dotnet_diagnostic.CA1871.severity = error # Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' +dotnet_diagnostic.CA1872.severity = none # Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' — covered by PSH1224 +dotnet_diagnostic.CA1873.severity = none # Avoid potentially expensive evaluation of arguments to 'Debug.Assert' — covered by PSH1417 +dotnet_diagnostic.CA1874.severity = none # Use 'Regex.IsMatch' — covered by PSH1406 +dotnet_diagnostic.CA1875.severity = none # Use 'Regex.Count' — covered by PSH1406 +dotnet_diagnostic.CA1877.severity = none # Use 'Encoding.GetString' instead of 'Encoding.GetChars' — covered by PSH1225 + +# Reliability +dotnet_diagnostic.CA2000.severity = suggestion # Dispose objects before losing scope +dotnet_diagnostic.CA2002.severity = error # Do not lock on objects with weak identity +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task — Rx and library callers drive synchronization context themselves +dotnet_diagnostic.CA2008.severity = error # Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2009.severity = error # Do not call ToImmutableCollection on an ImmutableCollection value +dotnet_diagnostic.CA2011.severity = error # Do not assign property within its setter +dotnet_diagnostic.CA2012.severity = error # Use ValueTasks correctly +dotnet_diagnostic.CA2013.severity = error # Do not use ReferenceEquals with value types +dotnet_diagnostic.CA2014.severity = error # Do not use stackalloc in loops +dotnet_diagnostic.CA2015.severity = error # Do not define finalizers for types derived from MemoryManager +dotnet_diagnostic.CA2016.severity = error # Forward the CancellationToken parameter to methods that take one +dotnet_diagnostic.CA2017.severity = error # Parameter count mismatch +dotnet_diagnostic.CA2018.severity = error # The 'count' argument to 'Buffer.BlockCopy' should specify the number of bytes to copy +dotnet_diagnostic.CA2019.severity = error # Improper 'ThreadStatic' field initialization +dotnet_diagnostic.CA2020.severity = error # Prevent behavioral change caused by built-in operators of IntPtr/UIntPtr +dotnet_diagnostic.CA2021.severity = error # Don't call Enumerable.Cast or Enumerable.OfType with incompatible types +dotnet_diagnostic.CA2022.severity = error # Avoid inexact read with 'Stream.Read' +dotnet_diagnostic.CA2023.severity = error # Invalid braces in message template +dotnet_diagnostic.CA2024.severity = error # Do not use 'StreamReader.EndOfStream' in async methods +dotnet_diagnostic.CA2025.severity = error # Do not pass 'IDisposable' instances into unawaited tasks +dotnet_diagnostic.CA2026.severity = error # Do not use methods or types annotated with [RequiresDynamicCode] in code that uses [RequiresDynamicCode] + +# Usage +dotnet_diagnostic.CA1801.severity = error # Review unused parameters +dotnet_code_quality.CA1801.api_surface = private, internal # only flag non-public APIs so we don't break public signatures +dotnet_diagnostic.CA1816.severity = error # Call GC.SuppressFinalize correctly +dotnet_diagnostic.CA2200.severity = none # Rethrow to preserve stack details — covered by SST1430 +dotnet_diagnostic.CA2201.severity = error # Do not raise reserved exception types +dotnet_diagnostic.CA2207.severity = error # Initialize value type static fields inline +dotnet_diagnostic.CA2208.severity = none # Instantiate argument exceptions correctly — too sensitive: flags valid context-forwarding nameof(arg.Property) patterns +dotnet_diagnostic.CA2211.severity = none # Non-constant fields should not be visible — covered by SST1499 +dotnet_diagnostic.CA2213.severity = error # Disposable fields should be disposed +dotnet_diagnostic.CA2214.severity = none # Do not call overridable methods in constructors — covered by SST1483 +dotnet_diagnostic.CA2215.severity = error # Dispose methods should call base class dispose +dotnet_diagnostic.CA2216.severity = none # Disposable types should declare finalizer — conflicts with SST2317, which owns the owned-native-handle shape and prescribes a SafeHandle instead of a finalizer +dotnet_diagnostic.CA2217.severity = none # Do not mark enums with FlagsAttribute — covered by SST2303 +dotnet_diagnostic.CA2218.severity = error # Override GetHashCode on overriding Equals +dotnet_diagnostic.CA2219.severity = error # Do not raise exceptions in finally clauses +dotnet_diagnostic.CA2224.severity = none # Override Equals on overloading operator equals — covered by SST2302 +dotnet_diagnostic.CA2225.severity = error # Operator overloads have named alternates +dotnet_diagnostic.CA2226.severity = error # Operators should have symmetrical overloads +dotnet_diagnostic.CA2227.severity = none # Collection properties should be read only — settable collection properties are common in our DTOs and config types +dotnet_diagnostic.CA2231.severity = error # Overload operator equals on overriding ValueType.Equals +dotnet_diagnostic.CA2234.severity = error # Pass System.Uri objects instead of strings +dotnet_diagnostic.CA2241.severity = error # Provide correct arguments to formatting methods +dotnet_diagnostic.CA2242.severity = none # Test for NaN correctly — covered by SST1473 +dotnet_diagnostic.CA2243.severity = error # Attribute string literals should parse correctly +dotnet_diagnostic.CA2244.severity = error # Do not duplicate indexed element initializations +dotnet_diagnostic.CA2245.severity = none # Do not assign a property to itself — covered by SST1189 +dotnet_diagnostic.CA2246.severity = error # Do not assign a symbol and its member in the same statement +dotnet_diagnostic.CA2247.severity = error # Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum +dotnet_diagnostic.CA2248.severity = error # Provide correct enum argument to Enum.HasFlag +dotnet_diagnostic.CA2249.severity = error # Use String.Contains instead of String.IndexOf for substring checks +dotnet_diagnostic.CA2250.severity = error # Use ThrowIfCancellationRequested +dotnet_diagnostic.CA2251.severity = none # Covered by PSH1216 (canonical) +dotnet_diagnostic.CA2252.severity = error # Opt in to preview features before using them +dotnet_diagnostic.CA2253.severity = error # Named placeholders should not be numeric values +dotnet_diagnostic.CA2254.severity = error # Template should be a static expression +dotnet_diagnostic.CA2255.severity = error # The 'ModuleInitializer' attribute should not be used in libraries +dotnet_diagnostic.CA2256.severity = error # All members declared in parent interfaces must have an implementation in a 'DynamicInterfaceCastableImplementation' interface +dotnet_diagnostic.CA2257.severity = error # Members defined in a type with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +dotnet_diagnostic.CA2258.severity = error # Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +dotnet_diagnostic.CA2259.severity = error # 'ThreadStatic' only affects static fields +dotnet_diagnostic.CA2260.severity = error # Implement generic math interfaces correctly +dotnet_diagnostic.CA2261.severity = error # Do not use 'ConfigureAwaitOptions.SuppressThrowing' with 'Task' +dotnet_diagnostic.CA2262.severity = error # Set 'MaxResponseHeadersLength' properly +dotnet_diagnostic.CA2263.severity = error # Prefer generic overload when type is known +dotnet_diagnostic.CA2264.severity = error # Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' +dotnet_diagnostic.CA2265.severity = error # Do not compare Span to 'null' or 'default' +dotnet_diagnostic.CA2266.severity = error # Do not consume the result of 'Span.Enumerator.MoveNext()' more than once +dotnet_diagnostic.CA2267.severity = error # Member should not be marked with 'StaticAttribute' +dotnet_diagnostic.CA2268.severity = error # Use 'string.Equals(string, string, StringComparison)' instead of the implicit ordinal overload +# Skipped (deprecated ISerializable formatter): CA2229 Implement serialization constructors, +# CA2235 Mark all non-serializable fields, CA2237 Mark ISerializable types with SerializableAttribute. + +# Security + +# SQL Injection & Command Injection +dotnet_diagnostic.CA2100.severity = error # Review SQL queries for security vulnerabilities +dotnet_diagnostic.CA3001.severity = error # Review code for SQL injection vulnerabilities +dotnet_diagnostic.CA3006.severity = error # Review code for process command injection vulnerabilities + +# Cross-Site Scripting (XSS) & Injection Attacks +dotnet_diagnostic.CA3002.severity = error # Review code for XSS vulnerabilities +dotnet_diagnostic.CA3003.severity = error # Review code for file path injection vulnerabilities +dotnet_diagnostic.CA3005.severity = error # Review code for LDAP injection vulnerabilities +dotnet_diagnostic.CA3007.severity = error # Review code for open redirect vulnerabilities +dotnet_diagnostic.CA3008.severity = error # Review code for XPath injection vulnerabilities +dotnet_diagnostic.CA3009.severity = error # Review code for XML injection vulnerabilities +dotnet_diagnostic.CA3010.severity = error # Review code for XAML injection vulnerabilities +dotnet_diagnostic.CA3011.severity = error # Review code for DLL injection vulnerabilities +dotnet_diagnostic.CA3012.severity = error # Review code for regex injection vulnerabilities +dotnet_diagnostic.CA3004.severity = error # Review code for information disclosure vulnerabilities + +# Insecure Deserialization +dotnet_diagnostic.CA2300.severity = error # Do not use insecure deserializer BinaryFormatter +dotnet_diagnostic.CA2301.severity = error # Do not call BinaryFormatter.Deserialize without setting Binder +dotnet_diagnostic.CA2302.severity = error # Ensure BinaryFormatter.Binder is set before deserializing +dotnet_diagnostic.CA2305.severity = error # Do not use insecure deserializer LosFormatter +dotnet_diagnostic.CA2310.severity = error # Do not use insecure deserializer NetDataContractSerializer +dotnet_diagnostic.CA2311.severity = error # Do not deserialize without setting NetDataContractSerializer.Binder +dotnet_diagnostic.CA2312.severity = error # Ensure NetDataContractSerializer.Binder is set before deserializing +dotnet_diagnostic.CA2315.severity = error # Do not use insecure deserializer ObjectStateFormatter +dotnet_diagnostic.CA2321.severity = error # Do not deserialize with JavaScriptSerializer using SimpleTypeResolver +dotnet_diagnostic.CA2322.severity = error # Ensure JavaScriptSerializer not initialized with SimpleTypeResolver +dotnet_diagnostic.CA2326.severity = error # Do not use TypeNameHandling values other than None +dotnet_diagnostic.CA2327.severity = error # Do not use insecure JsonSerializerSettings +dotnet_diagnostic.CA2328.severity = error # Ensure that JsonSerializerSettings are secure +dotnet_diagnostic.CA2329.severity = error # Do not deserialize with JsonSerializer using insecure configuration +dotnet_diagnostic.CA2330.severity = error # Ensure JsonSerializer has secure configuration when deserializing +dotnet_diagnostic.CA2350.severity = error # Ensure DataTable.ReadXml()'s input is trusted +dotnet_diagnostic.CA2351.severity = error # Ensure DataSet.ReadXml()'s input is trusted +dotnet_diagnostic.CA2352.severity = error # Unsafe DataSet/DataTable in serializable type vulnerable to RCE +dotnet_diagnostic.CA2353.severity = error # Unsafe DataSet or DataTable in serializable type +dotnet_diagnostic.CA2354.severity = error # Unsafe DataSet/DataTable in deserialized object graph vulnerable to RCE +dotnet_diagnostic.CA2355.severity = error # Unsafe DataSet or DataTable in deserialized object graph +dotnet_diagnostic.CA2356.severity = error # Unsafe DataSet/DataTable in web deserialized object graph +dotnet_diagnostic.CA2361.severity = error # Ensure autogenerated class with DataSet.ReadXml() not used with untrusted data +dotnet_diagnostic.CA2362.severity = error # Unsafe DataSet/DataTable in autogenerated serializable type vulnerable to RCE +dotnet_diagnostic.CA5360.severity = error # Do not call dangerous methods in deserialization +dotnet_diagnostic.CA5362.severity = error # Potential reference cycle in deserialized object graph + +# Cryptography - Weak & Broken Algorithms +dotnet_diagnostic.CA5350.severity = error # Do not use weak cryptographic algorithms (SHA1, RIPEMD160, TripleDES) +dotnet_diagnostic.CA5351.severity = error # Do not use broken cryptographic algorithms (MD5, DES, RC2) +dotnet_diagnostic.CA5358.severity = error # Do not use unsafe cipher modes (ECB, OFB, CFB) +dotnet_diagnostic.CA5373.severity = error # Do not use obsolete key derivation function +dotnet_diagnostic.CA5379.severity = error # Ensure key derivation function algorithm is sufficiently strong +dotnet_diagnostic.CA5384.severity = error # Do not use Digital Signature Algorithm (DSA) +dotnet_diagnostic.CA5385.severity = error # Use RSA algorithm with sufficient key size (>= 2048 bits) +dotnet_diagnostic.CA5387.severity = error # Do not use weak key derivation function with insufficient iteration count +dotnet_diagnostic.CA5388.severity = error # Ensure sufficient iteration count when using weak key derivation function +dotnet_diagnostic.CA5390.severity = error # Do not hard-code encryption key +dotnet_diagnostic.CA5394.severity = error # Do not use insecure randomness (use RNGCryptoServiceProvider) +dotnet_diagnostic.CA5401.severity = error # Do not use CreateEncryptor with non-default IV +dotnet_diagnostic.CA5402.severity = error # Use CreateEncryptor with the default IV +dotnet_diagnostic.CA5403.severity = error # Do not hard-code certificate + +# TLS/SSL Protocol Security +dotnet_diagnostic.CA5359.severity = error # Do not disable certificate validation +dotnet_diagnostic.CA5361.severity = error # Do not disable SChannel use of strong crypto +dotnet_diagnostic.CA5364.severity = error # Do not use deprecated security protocols (TLS 1.0, TLS 1.1, SSL3) +dotnet_diagnostic.CA5378.severity = error # Do not disable ServicePointManagerSecurityProtocols +dotnet_diagnostic.CA5380.severity = error # Do not add certificates to root store +dotnet_diagnostic.CA5381.severity = error # Ensure certificates are not added to root store +dotnet_diagnostic.CA5386.severity = error # Avoid hardcoding SecurityProtocolType value +dotnet_diagnostic.CA5397.severity = error # Do not use deprecated SslProtocols values +dotnet_diagnostic.CA5398.severity = error # Avoid hardcoded SslProtocols values +dotnet_diagnostic.CA5399.severity = error # Definitely disable HttpClient certificate revocation list check +dotnet_diagnostic.CA5400.severity = error # Ensure HttpClient certificate revocation list check is not disabled + +# Azure Storage / Shared Access Signature +dotnet_diagnostic.CA5375.severity = error # Do not use account shared access signature +dotnet_diagnostic.CA5376.severity = error # Use SharedAccessProtocol HttpsOnly +dotnet_diagnostic.CA5377.severity = error # Use container level access policy + +# XML Security +dotnet_diagnostic.CA3061.severity = error # Do not add schema by URL +dotnet_diagnostic.CA3075.severity = error # Insecure DTD processing +dotnet_diagnostic.CA3076.severity = error # Insecure XSLT script execution +dotnet_diagnostic.CA3077.severity = error # Insecure processing in API design, XML Document and XML Text Reader +dotnet_diagnostic.CA5366.severity = error # Use XmlReader for DataSet read XML +dotnet_diagnostic.CA5369.severity = error # Use XmlReader for deserialize +dotnet_diagnostic.CA5370.severity = error # Use XmlReader for validating reader +dotnet_diagnostic.CA5371.severity = error # Use XmlReader for schema read +dotnet_diagnostic.CA5372.severity = error # Use XmlReader for XPathDocument +dotnet_diagnostic.CA5374.severity = error # Do not use XslTransform + +# Web Security +dotnet_diagnostic.CA3147.severity = error # Mark verb handlers with ValidateAntiForgeryToken +dotnet_diagnostic.CA5363.severity = error # Do not disable request validation +dotnet_diagnostic.CA5365.severity = error # Do not disable HTTP header checking +dotnet_diagnostic.CA5368.severity = error # Set ViewStateUserKey for classes derived from Page +dotnet_diagnostic.CA5382.severity = error # Use secure cookies in ASP.NET Core +dotnet_diagnostic.CA5383.severity = error # Ensure use of secure cookies in ASP.NET Core +dotnet_diagnostic.CA5391.severity = error # Use antiforgery tokens in ASP.NET Core MVC controllers +dotnet_diagnostic.CA5395.severity = error # Miss HttpVerb attribute for action methods +dotnet_diagnostic.CA5396.severity = error # Set HttpOnly to true for HttpCookie + +# P/Invoke & DLL Security +dotnet_diagnostic.CA2101.severity = error # Specify marshalling for P/Invoke string arguments +dotnet_diagnostic.CA5392.severity = error # Use DefaultDllImportSearchPaths attribute for P/Invokes +dotnet_diagnostic.CA5393.severity = error # Do not use unsafe DllImportSearchPath value + +# Archive & File Security +dotnet_diagnostic.CA5389.severity = error # Do not add archive item's path to target file system path (Zip Slip) + +# Token Validation & Authentication +dotnet_diagnostic.CA5404.severity = error # Do not disable token validation checks +dotnet_diagnostic.CA5405.severity = error # Do not always skip token validation in delegates + +# Other Security Rules +dotnet_diagnostic.CA2109.severity = error # Review visible event handlers +dotnet_diagnostic.CA2119.severity = error # Seal methods that satisfy private interfaces +dotnet_diagnostic.CA2153.severity = error # Do not catch corrupted state exceptions +dotnet_diagnostic.CA5367.severity = error # Do not serialize types with pointer fields + +################### +# Microsoft .NET SDK Diagnostics (SYSLIB) +################### +# Runtime obsoletions +dotnet_diagnostic.SYSLIB0001.severity = error # The UTF-7 encoding is insecure and should not be used +dotnet_diagnostic.SYSLIB0002.severity = error # PrincipalPermissionAttribute is not honored by the runtime and must not be used +dotnet_diagnostic.SYSLIB0003.severity = error # Code Access Security (CAS) is not supported or honored by the runtime +dotnet_diagnostic.SYSLIB0004.severity = error # Constrained Execution Region (CER) feature is not supported +dotnet_diagnostic.SYSLIB0005.severity = error # Global Assembly Cache is not supported +dotnet_diagnostic.SYSLIB0006.severity = error # Thread.Abort is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0007.severity = error # The default implementation of this cryptography algorithm is not supported +dotnet_diagnostic.SYSLIB0008.severity = error # The CreatePdbGenerator API is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0009.severity = error # The AuthenticationManager Authenticate and PreAuthenticate methods are not supported +dotnet_diagnostic.SYSLIB0010.severity = error # This Remoting API is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0011.severity = error # BinaryFormatter serialization is obsolete +dotnet_diagnostic.SYSLIB0012.severity = error # Assembly.CodeBase and Assembly.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0013.severity = error # Uri.EscapeUriString can corrupt the Uri string +dotnet_diagnostic.SYSLIB0014.severity = error # WebRequest, HttpWebRequest, ServicePoint, and WebClient are obsolete — use HttpClient +dotnet_diagnostic.SYSLIB0015.severity = error # DisablePrivateReflectionAttribute is not honored +dotnet_diagnostic.SYSLIB0016.severity = error # Use Marshal.GetExceptionPointers instead +dotnet_diagnostic.SYSLIB0017.severity = error # Strong name signing is not supported on .NET Core +dotnet_diagnostic.SYSLIB0018.severity = error # ReflectionOnly loading is not supported +dotnet_diagnostic.SYSLIB0019.severity = error # RuntimeEnvironment members SystemConfigurationFile, GetSystemVersion, and FromGlobalAccessCache are not supported +dotnet_diagnostic.SYSLIB0020.severity = error # JsonSerializerOptions.IgnoreNullValues is obsolete — use DefaultIgnoreCondition +dotnet_diagnostic.SYSLIB0021.severity = error # Derived cryptographic types are obsolete — use the Create factory methods +dotnet_diagnostic.SYSLIB0022.severity = error # Rijndael types are obsolete — use Aes +dotnet_diagnostic.SYSLIB0023.severity = error # RNGCryptoServiceProvider is obsolete — use RandomNumberGenerator +dotnet_diagnostic.SYSLIB0024.severity = error # Creating and unloading AppDomains is not supported and throws an exception +dotnet_diagnostic.SYSLIB0025.severity = error # SuppressIldasmAttribute has no effect in .NET 6.0+ +dotnet_diagnostic.SYSLIB0026.severity = error # X509Certificate and X509Certificate2 parameterless constructors are obsolete +dotnet_diagnostic.SYSLIB0027.severity = error # PublicKey.Key is obsolete — use the appropriate method to get the public key +dotnet_diagnostic.SYSLIB0028.severity = error # X509Certificate2.PrivateKey is obsolete — use the appropriate method to get the private key +dotnet_diagnostic.SYSLIB0029.severity = error # ProduceLegacyHmacValues is obsolete +dotnet_diagnostic.SYSLIB0030.severity = error # HMACSHA1 always uses the algorithm implementation provided by the platform +dotnet_diagnostic.SYSLIB0031.severity = error # EncodingProvider.GetEncoding(int, EncoderFallback, DecoderFallback) is obsolete +dotnet_diagnostic.SYSLIB0032.severity = error # Recovery from corrupted process state exceptions is not supported +dotnet_diagnostic.SYSLIB0033.severity = error # Rfc2898DeriveBytes.CryptDeriveKey is obsolete +dotnet_diagnostic.SYSLIB0034.severity = error # CmsSigner has been deprecated — use a constructor that accepts a SubjectIdentifierType +dotnet_diagnostic.SYSLIB0035.severity = error # ComputeCounterSignature without specifying a CmsSigner is obsolete +dotnet_diagnostic.SYSLIB0036.severity = error # Regex.CompileToAssembly is obsolete and not supported — use the RegexGenerator source generator +dotnet_diagnostic.SYSLIB0037.severity = error # AssemblyName members HashAlgorithm, ProcessorArchitecture, and VersionCompatibility are obsolete +dotnet_diagnostic.SYSLIB0038.severity = error # SerializationFormat.Binary is obsolete +dotnet_diagnostic.SYSLIB0039.severity = error # TLS versions 1.0 and 1.1 have known vulnerabilities and are not recommended +dotnet_diagnostic.SYSLIB0040.severity = error # EncryptionPolicy.NoEncryption and AllowEncryption significantly reduce security +dotnet_diagnostic.SYSLIB0041.severity = error # The default iteration count for Rfc2898DeriveBytes parameters is outdated and insecure +dotnet_diagnostic.SYSLIB0042.severity = error # ToXmlString and FromXmlString have no implementation for ECC types +dotnet_diagnostic.SYSLIB0043.severity = error # ECDiffieHellmanPublicKey.ToByteArray() and the associated constructor do not have a consistent and interoperable implementation +dotnet_diagnostic.SYSLIB0044.severity = error # AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0045.severity = error # Cryptographic factory methods accepting an algorithm name are obsolete +dotnet_diagnostic.SYSLIB0046.severity = error # ControlledExecution.Run may corrupt the process and should not be used in production code +dotnet_diagnostic.SYSLIB0047.severity = error # XmlSecureResolver is obsolete +dotnet_diagnostic.SYSLIB0048.severity = error # RSA.EncryptValue and RSA.DecryptValue are not supported and throw NotSupportedException +dotnet_diagnostic.SYSLIB0049.severity = error # JsonSerializerOptions.AddContext is obsolete +dotnet_diagnostic.SYSLIB0050.severity = error # Formatter-based serialization (ISerializable, FormatterServices, etc.) is obsolete +dotnet_diagnostic.SYSLIB0051.severity = error # The Exception API ISerializable / GetObjectData formatter-based serialization is obsolete +dotnet_diagnostic.SYSLIB0052.severity = error # Members for assigning to AssemblyName.CodeBase or AssemblyName.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0053.severity = error # AesGcm should indicate the required tag size for encryption and decryption +dotnet_diagnostic.SYSLIB0054.severity = error # Strong name signing is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0055.severity = error # 'Memory.Pin' may be incorrectly used with structures +dotnet_diagnostic.SYSLIB0056.severity = error # LoadFromHash and related members are obsolete +dotnet_diagnostic.SYSLIB0057.severity = error # Use X509CertificateLoader instead to load certificates +dotnet_diagnostic.SYSLIB0058.severity = error # KeyExchangeAlgorithm and KeyExchangeStrength are obsolete on SslStream +dotnet_diagnostic.SYSLIB0059.severity = error # SystemEvents.EventsThreadShutdown event is obsolete +dotnet_diagnostic.SYSLIB0060.severity = error # Constructors of DirectoryServices.ActiveDirectory.ConfigurationContext are obsolete +dotnet_diagnostic.SYSLIB0061.severity = error # CryptographyConfig.AddOID and AddAlgorithm methods are obsolete + +# Source generators +# LoggerMessage source generator +dotnet_diagnostic.SYSLIB1001.severity = error # Logging method names cannot start with _ +dotnet_diagnostic.SYSLIB1002.severity = error # Don't include log level parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1003.severity = error # LoggerMessage attribute log level value must be defined +dotnet_diagnostic.SYSLIB1004.severity = error # Logging class cannot be in nested types +dotnet_diagnostic.SYSLIB1005.severity = error # Could not find a required type definition +dotnet_diagnostic.SYSLIB1006.severity = error # Multiple logging methods cannot use the same event ID within a class +dotnet_diagnostic.SYSLIB1007.severity = error # Logging methods must return void +dotnet_diagnostic.SYSLIB1008.severity = error # One of the arguments to a logging method must implement ILogger +dotnet_diagnostic.SYSLIB1009.severity = error # Logging methods must be static +dotnet_diagnostic.SYSLIB1010.severity = error # Logging methods must be partial +dotnet_diagnostic.SYSLIB1011.severity = error # Logging methods cannot be generic +dotnet_diagnostic.SYSLIB1012.severity = error # Logging methods must not have a body +dotnet_diagnostic.SYSLIB1013.severity = error # Missing logger argument to logging method +dotnet_diagnostic.SYSLIB1015.severity = error # Argument is not referenced from the logging message +dotnet_diagnostic.SYSLIB1016.severity = error # Logging methods cannot have a parameter named the same as one of the template parameters +dotnet_diagnostic.SYSLIB1017.severity = error # Logging methods cannot have more than one parameter of type LogLevel +dotnet_diagnostic.SYSLIB1018.severity = error # Logging method parameter doesn't appear in template +dotnet_diagnostic.SYSLIB1019.severity = error # Couldn't find a field of type ILogger +dotnet_diagnostic.SYSLIB1020.severity = error # Found multiple fields of type ILogger +dotnet_diagnostic.SYSLIB1021.severity = error # Cannot have multiple logging methods with the same name +dotnet_diagnostic.SYSLIB1022.severity = error # Logging methods cannot have multiple template parameters with the same name +dotnet_diagnostic.SYSLIB1023.severity = error # Generating more than 6 arguments is not supported +dotnet_diagnostic.SYSLIB1024.severity = error # Logging method template name conflicts with a method parameter name +dotnet_diagnostic.SYSLIB1025.severity = error # Multiple template parameters with the same name +# Skipped SYSLIB1014 (Reserved) + +# System.Text.Json source generator +dotnet_diagnostic.SYSLIB1030.severity = error # JsonSourceGenerator did not generate serialization metadata for type +dotnet_diagnostic.SYSLIB1031.severity = error # Duplicate type registration in JsonSerializerContext +dotnet_diagnostic.SYSLIB1032.severity = error # JsonSerializerContext classes must be partial +dotnet_diagnostic.SYSLIB1033.severity = error # Serializable types must have at least one constructor +dotnet_diagnostic.SYSLIB1034.severity = error # Type uses an unsupported [JsonExtensionData] property +dotnet_diagnostic.SYSLIB1036.severity = error # Init-only properties are not supported in source generation +dotnet_diagnostic.SYSLIB1037.severity = error # Source generator does not support [JsonInclude] on inaccessible members +dotnet_diagnostic.SYSLIB1038.severity = error # JsonInclude on a non-public property is not supported +dotnet_diagnostic.SYSLIB1039.severity = error # Type with extension data of unsupported shape +dotnet_diagnostic.SYSLIB1040.severity = error # Type with cycles is not supported +dotnet_diagnostic.SYSLIB1041.severity = error # Duplicate type names registered with JsonSerializerContext +# Skipped SYSLIB1035 (Reserved) + +# GeneratedRegex source generator +dotnet_diagnostic.SYSLIB1042.severity = error # Use 'GeneratedRegexAttribute' for static regex creation +dotnet_diagnostic.SYSLIB1043.severity = error # The regex pattern is not valid +dotnet_diagnostic.SYSLIB1044.severity = error # The regex source generator failed +dotnet_diagnostic.SYSLIB1045.severity = error # Convert to 'GeneratedRegexAttribute' for compile-time regex generation +dotnet_diagnostic.SYSLIB1046.severity = error # Could not find an applicable Regex constructor +dotnet_diagnostic.SYSLIB1047.severity = error # The regex options are not valid for source generation +dotnet_diagnostic.SYSLIB1048.severity = error # GeneratedRegex source generator encountered an unhandled error +dotnet_diagnostic.SYSLIB1049.severity = error # Multiple regex source generators conflict + +# LibraryImport (P/Invoke) source generator +dotnet_diagnostic.SYSLIB1050.severity = error # Method should be marked 'LibraryImport' instead of 'DllImport' +dotnet_diagnostic.SYSLIB1051.severity = error # Specified type is not supported by source-generated P/Invokes +dotnet_diagnostic.SYSLIB1052.severity = error # Specified configuration is not supported by source-generated P/Invokes +dotnet_diagnostic.SYSLIB1053.severity = error # 'LibraryImportAttribute' requires unsafe code +dotnet_diagnostic.SYSLIB1054.severity = error # Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time +dotnet_diagnostic.SYSLIB1055.severity = error # Invalid 'CustomMarshallerAttribute' usage +dotnet_diagnostic.SYSLIB1056.severity = error # Specified marshaller type is invalid +dotnet_diagnostic.SYSLIB1057.severity = error # Marshaller type does not have the required shape +dotnet_diagnostic.SYSLIB1058.severity = error # Invalid 'NativeMarshallingAttribute' usage +dotnet_diagnostic.SYSLIB1059.severity = error # Marshaller type must be a closed generic type +dotnet_diagnostic.SYSLIB1060.severity = error # Specified native type is invalid +dotnet_diagnostic.SYSLIB1061.severity = error # Marshaller type has incompatible method signatures +dotnet_diagnostic.SYSLIB1062.severity = error # Project must be updated with 'true' +dotnet_diagnostic.SYSLIB1063.severity = error # 'CustomMarshaller' types must have an 'in' or 'ref' first parameter +dotnet_diagnostic.SYSLIB1064.severity = error # Forwarder marshaller types should not have 'GenericPlaceholder' + +# GeneratedComInterface / COM source generator +dotnet_diagnostic.SYSLIB1090.severity = error # Convert to 'GeneratedComInterface' +dotnet_diagnostic.SYSLIB1091.severity = error # Type is an invalid 'GeneratedComInterface' base interface +dotnet_diagnostic.SYSLIB1092.severity = error # 'GeneratedComInterface' must specify 'Guid' +dotnet_diagnostic.SYSLIB1093.severity = error # Method on 'GeneratedComInterface' has unsupported return type +dotnet_diagnostic.SYSLIB1094.severity = error # Method on 'GeneratedComInterface' has unsupported parameter +dotnet_diagnostic.SYSLIB1095.severity = error # Type implements 'GeneratedComInterface' but is not partial +dotnet_diagnostic.SYSLIB1096.severity = error # 'GeneratedComClassAttribute' must be applied to a partial type +dotnet_diagnostic.SYSLIB1097.severity = error # 'GeneratedComClassAttribute' type must implement at least one 'GeneratedComInterface' +dotnet_diagnostic.SYSLIB1098.severity = error # 'GeneratedComInterface' interface inheritance not supported + +# Configuration binding source generator +dotnet_diagnostic.SYSLIB1100.severity = error # Configuration binding source generator: type is not supported +dotnet_diagnostic.SYSLIB1101.severity = error # Configuration binding source generator: property on type is not supported +dotnet_diagnostic.SYSLIB1102.severity = error # Configuration binding source generator: not enabled +dotnet_diagnostic.SYSLIB1103.severity = error # Configuration binding source generator: type lacks a parameterless constructor +dotnet_diagnostic.SYSLIB1104.severity = error # Configuration binding source generator: language version is too low + +################### +# Microsoft .NET Code Style Analyzers (IDE) +################### +# Language rules +# EnforceCodeStyleInBuild=true (set in Directory.Build.props) promotes these +# IDE rules into `dotnet build` so they fire at compile time, not just in the +# IDE. We enable the ones that are unambiguous wins and skip the rules that +# conflict with an existing convention (SA1101, SA1200, SA1206, SA1400, SA1633, +# etc.) so we don't double-report. + +# Bug catchers — always error. +dotnet_diagnostic.IDE0035.severity = none # Remove unreachable code — covered by SST1453 +dotnet_diagnostic.IDE0043.severity = none # Format string contains invalid placeholder — covered by SST1454 +dotnet_diagnostic.IDE0052.severity = none # Remove unread private member — covered by SST1441 + +# Simplification / cleanup. +dotnet_diagnostic.IDE0002.severity = none # Simplify member access — covered by SST1117 +dotnet_diagnostic.IDE0004.severity = none # Remove unnecessary cast — covered by SST1175 +dotnet_diagnostic.IDE0016.severity = none # Use throw expression — covered by SST2207 +dotnet_diagnostic.IDE0019.severity = none # Use pattern matching to avoid 'as' followed by a 'null' check — covered by SST2005/SST2274 +dotnet_diagnostic.IDE0028.severity = none # Use collection initializers — covered by SST1194 +dotnet_diagnostic.IDE0031.severity = none # Use null propagation — covered by SST1196 +dotnet_diagnostic.IDE0038.severity = none # Use pattern matching ('is' check without a cast) — covered by SST2007 +dotnet_diagnostic.IDE0041.severity = none # Use 'is null' check — covered by SST1149/SST2231/SST2282 +dotnet_diagnostic.IDE0042.severity = none # Deconstruct variable declaration — covered by SST2214 +dotnet_diagnostic.IDE0044.severity = none # Add readonly modifier — covered by SST1424 +dotnet_diagnostic.IDE0047.severity = none # Remove unnecessary parentheses — covered by SST1459 +dotnet_diagnostic.IDE0049.severity = none # Use language keywords instead of framework type names for type references — covered by SST1121 +dotnet_diagnostic.IDE0057.severity = none # Use range operator — covered by SST2204 +dotnet_diagnostic.IDE0059.severity = none # Remove unnecessary value assignment — covered by SST2222 +dotnet_diagnostic.IDE0064.severity = none # Make struct fields writable — flag readonly-but-mutable struct fields where intent diverges from declaration +dotnet_diagnostic.IDE0066.severity = none # Use switch expression — covered by SST2201 +dotnet_diagnostic.IDE0075.severity = none # Simplify conditional expression — covered by SST1182 +dotnet_diagnostic.IDE0078.severity = none # Use pattern matching — covered by SST2006/SST2231 +dotnet_diagnostic.IDE0084.severity = none # Use pattern matching ('IsNot' operator) +dotnet_diagnostic.IDE0120.severity = none # Simplify LINQ expression — covered by PSH1100/PSH1101 +dotnet_diagnostic.IDE0221.severity = none # Add explicit cast — covered by SST2226 +dotnet_diagnostic.IDE0250.severity = none # Make struct 'readonly' — covered by PSH1014 +dotnet_diagnostic.IDE0260.severity = none # Use pattern matching — covered by SST2006/SST2231 + +# Disabled — conflict with an existing SA/CA rule or project convention. +dotnet_diagnostic.IDE0007.severity = none # Use var — we don't force var in either direction +dotnet_diagnostic.IDE0008.severity = none # Use explicit type — we don't force var in either direction +dotnet_diagnostic.IDE0009.severity = none # Member access should be qualified — SA1101 = none +dotnet_diagnostic.IDE0021.severity = none # Use expression body for constructors — covered by SST2276 (opt-in) +dotnet_diagnostic.IDE0022.severity = none # Use expression body for methods — covered by SST2275 +dotnet_diagnostic.IDE0023.severity = none # Use expression body for conversion operators — covered by SST2278 (opt-in) +dotnet_diagnostic.IDE0024.severity = none # Use expression body for operators — covered by SST2277 (opt-in) +dotnet_diagnostic.IDE0025.severity = none # Use expression body for properties — covered by SST2279 +dotnet_diagnostic.IDE0026.severity = none # Use expression body for indexers — covered by SST2280 +dotnet_diagnostic.IDE0048.severity = none # Add parentheses for clarity — preference not enforced +dotnet_diagnostic.IDE0055.severity = none # Formatting — StyleCop SA rules own formatting +dotnet_diagnostic.IDE0058.severity = none # Remove unnecessary expression value — covered by SST2221 +dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter — CA1801 handles (with api_surface config) +dotnet_diagnostic.IDE0061.severity = none # Use expression body for local function — covered by SST2281 +dotnet_diagnostic.IDE0079.severity = none # Remove unnecessary suppression — can misfire on multi-TFM suppressions +dotnet_diagnostic.IDE0130.severity = none # Namespace does not match folder structure — we don't enforce strict mirror +dotnet_diagnostic.IDE0160.severity = none # Use block-scoped namespace — we use file-scoped (IDE0161) +dotnet_diagnostic.IDE0210.severity = none # Convert to top-level statements — we use Main style +dotnet_diagnostic.IDE0211.severity = none # Convert to 'Program.Main' style — we use Main style +dotnet_diagnostic.IDE0300.severity = none # Use collection expression for array — covered by SST2101 +dotnet_diagnostic.IDE0306.severity = none # Use collection expression for new — covered by SST2101 +dotnet_diagnostic.IDE0320.severity = none # Make anonymous function static — covered by PSH1000 +dotnet_diagnostic.IDE0050.severity = none # Convert anonymous type to tuple — covered by SST2224 +dotnet_diagnostic.IDE0310.severity = none # Convert lambda expression to method group — handled by RCS1207 +dotnet_diagnostic.IDE0360.severity = none # Simplify property accessor — covered by SST2219 +dotnet_diagnostic.IDE0370.severity = none # Remove unnecessary suppression (null-forgiving operator) — disabled for the same reason as RCS1249: multi-TFM nullability annotations can differ per platform + +# Language and unnecessary code rules. +dotnet_diagnostic.IDE0001.severity = none # Simplify name +dotnet_diagnostic.IDE0003.severity = none # Name can be simplified +dotnet_diagnostic.IDE0005.severity = none # Remove unnecessary import +dotnet_diagnostic.IDE0010.severity = none # Add missing cases to switch statement +dotnet_diagnostic.IDE0011.severity = none # Add braces +dotnet_diagnostic.IDE0017.severity = none # Use object initializers +dotnet_diagnostic.IDE0018.severity = none # Inline variable declaration +dotnet_diagnostic.IDE0020.severity = none # Use pattern matching to avoid is check followed by a cast (with variable) +dotnet_diagnostic.IDE0027.severity = none # Use expression body for accessors +dotnet_diagnostic.IDE0029.severity = none # Null check can be simplified +dotnet_diagnostic.IDE0030.severity = none # Null check can be simplified +dotnet_diagnostic.IDE0032.severity = none # Use auto property +dotnet_diagnostic.IDE0033.severity = none # Use explicitly provided tuple name +dotnet_diagnostic.IDE0034.severity = none # Simplify default expression +dotnet_diagnostic.IDE0036.severity = none # Order modifiers +dotnet_diagnostic.IDE0037.severity = none # Use inferred member name +dotnet_diagnostic.IDE0039.severity = none # Use local function instead of lambda +dotnet_diagnostic.IDE0040.severity = none # Add accessibility modifiers +dotnet_diagnostic.IDE0045.severity = none # Use conditional expression for assignment +dotnet_diagnostic.IDE0046.severity = none # Use conditional expression for return +dotnet_diagnostic.IDE0051.severity = none # Remove unused private member +dotnet_diagnostic.IDE0053.severity = none # Use expression body for lambdas +dotnet_diagnostic.IDE0054.severity = none # Use compound assignment +dotnet_diagnostic.IDE0056.severity = none # Use index operator +dotnet_diagnostic.IDE0062.severity = none # Make local function static +dotnet_diagnostic.IDE0063.severity = none # Use simple using statement +dotnet_diagnostic.IDE0065.severity = none # Using directive placement +dotnet_diagnostic.IDE0070.severity = none # Use System.HashCode.Combine +dotnet_diagnostic.IDE0071.severity = none # Simplify interpolation +dotnet_diagnostic.IDE0072.severity = none # Add missing cases to switch expression +dotnet_diagnostic.IDE0073.severity = none # Use file header +dotnet_diagnostic.IDE0074.severity = none # Use coalesce compound assignment +dotnet_diagnostic.IDE0076.severity = none # Remove invalid global SuppressMessageAttribute +dotnet_diagnostic.IDE0077.severity = none # Avoid legacy format target in global SuppressMessageAttribute +dotnet_diagnostic.IDE0080.severity = none # Remove unnecessary suppression operator +dotnet_diagnostic.IDE0082.severity = none # Convert typeof to nameof +dotnet_diagnostic.IDE0083.severity = none # Use pattern matching (not operator) +dotnet_diagnostic.IDE0090.severity = none # Simplify new expression +dotnet_diagnostic.IDE0100.severity = none # Remove unnecessary equality operator +dotnet_diagnostic.IDE0110.severity = none # Remove unnecessary discard +dotnet_diagnostic.IDE0150.severity = none # Prefer null check over type check +dotnet_diagnostic.IDE0161.severity = none # Use file-scoped namespace +dotnet_diagnostic.IDE0170.severity = none # Simplify property pattern +dotnet_diagnostic.IDE0180.severity = none # Use tuple to swap values +dotnet_diagnostic.IDE0200.severity = none # Remove unnecessary lambda expression +dotnet_diagnostic.IDE0220.severity = none # Add explicit cast in foreach loop +dotnet_diagnostic.IDE0230.severity = none # Use UTF-8 string literal +dotnet_diagnostic.IDE0240.severity = none # Nullable directive is redundant +dotnet_diagnostic.IDE0241.severity = none # Nullable directive is unnecessary +dotnet_diagnostic.IDE0251.severity = none # Member can be made readonly +dotnet_diagnostic.IDE0270.severity = none # Null check can be simplified +dotnet_diagnostic.IDE0280.severity = none # Use nameof +dotnet_diagnostic.IDE0290.severity = none # Use primary constructor +dotnet_diagnostic.IDE0301.severity = none # Use collection expression for empty +dotnet_diagnostic.IDE0302.severity = none # Use collection expression for stackalloc +dotnet_diagnostic.IDE0303.severity = none # Use collection expression for Create() +dotnet_diagnostic.IDE0304.severity = none # Use collection expression for builder +dotnet_diagnostic.IDE0305.severity = none # Use collection expression for fluent +dotnet_diagnostic.IDE0340.severity = none # Use unbound generic type +dotnet_diagnostic.IDE0350.severity = none # Use implicitly typed lambda +dotnet_diagnostic.IDE0380.severity = none # Remove unnecessary unsafe modifier +dotnet_diagnostic.IDE1005.severity = none # Use conditional delegate call + +# Naming and miscellaneous +# Naming conventions — SA1300 family already enforces PascalCase / interface +# prefix / field casing (with our _underscore convention on SA1306/1309/1311 +# deliberately disabled). Leaving IDE1006 off because configuring +# `dotnet_naming_rule.*` would duplicate what SA already enforces and could +# conflict with the _underscore convention. +dotnet_diagnostic.IDE1006.severity = none # Naming rule violation — SA1300 family handles naming +dotnet_diagnostic.IDE3000.severity = none # Disabled per project convention — not enforced + +################### +# Roslynator.CSharp.Analyzers (RCS) +################### +# Code simplification +dotnet_diagnostic.RCS1001.severity = none # Add braces (when expression spans over multiple lines) — covered by SST1519 +dotnet_diagnostic.RCS1003.severity = none # Add braces to if-else (when expression spans over multiple lines) — covered by SST1519 +dotnet_diagnostic.RCS1005.severity = error # Simplify nested using statement +dotnet_diagnostic.RCS1006.severity = none # Merge 'else' with nested 'if' — covered by SST1465 +dotnet_diagnostic.RCS1007.severity = error # Add braces +dotnet_diagnostic.RCS1031.severity = none # Remove unnecessary braces in switch section -- we don't mind braces in switch statements +dotnet_diagnostic.RCS1032.severity = error # Remove redundant parentheses +dotnet_diagnostic.RCS1033.severity = none # Remove redundant boolean literal — covered by SST1143 +dotnet_diagnostic.RCS1039.severity = none # Remove argument list from attribute — covered by SST1411 +dotnet_diagnostic.RCS1040.severity = none # Remove empty statement — covered by SST1180 +dotnet_diagnostic.RCS1042.severity = none # Remove enum default underlying type — covered by SST1177 +dotnet_diagnostic.RCS1043.severity = error # Remove 'partial' modifier from type with a single part +dotnet_diagnostic.RCS1049.severity = none # Simplify boolean comparison — covered by SST1143 +dotnet_diagnostic.RCS1058.severity = none # Use compound assignment — covered by SST1185 +dotnet_diagnostic.RCS1061.severity = none # Merge 'if' with nested 'if' — covered by SST2013 +dotnet_diagnostic.RCS1068.severity = none # Simplify logical negation — covered by SST1172/SST2006 +dotnet_diagnostic.RCS1069.severity = none # Remove unnecessary case label — covered by SST1466 +dotnet_diagnostic.RCS1070.severity = none # Remove redundant default switch section — covered by SST1179 +dotnet_diagnostic.RCS1071.severity = none # Remove redundant base constructor call — covered by SST1178 +dotnet_diagnostic.RCS1072.severity = none # Remove empty namespace declaration — covered by SST1435 +dotnet_diagnostic.RCS1073.severity = none # Convert 'if' to 'return' statement — covered by SST1197 +dotnet_diagnostic.RCS1074.severity = none # Remove redundant constructor — covered by SST1433 +dotnet_diagnostic.RCS1078.severity = none # Use "" or 'string.Empty' — conflicts with SST1122, which owns the string.Empty direction +dotnet_diagnostic.RCS1084.severity = none # Use coalesce expression instead of conditional expression — covered by SST1195 +dotnet_diagnostic.RCS1085.severity = none # Use auto-implemented property — covered by SST1420 +dotnet_diagnostic.RCS1089.severity = error # Use --/++ operator instead of assignment +dotnet_diagnostic.RCS1097.severity = error # Remove redundant 'ToString' call +dotnet_diagnostic.RCS1103.severity = error # Convert 'if' to assignment +dotnet_diagnostic.RCS1104.severity = none # Simplify conditional expression — covered by SST1182 +dotnet_diagnostic.RCS1105.severity = error # Unnecessary interpolation +dotnet_diagnostic.RCS1106.severity = none # Remove empty destructor — covered by PSH1002 +dotnet_diagnostic.RCS1107.severity = none # Remove redundant 'ToCharArray' call — covered by PSH1217 +dotnet_diagnostic.RCS1114.severity = error # Remove redundant delegate creation +dotnet_diagnostic.RCS1124.severity = error # Inline local variable +dotnet_diagnostic.RCS1126.severity = error # Add braces to if-else +dotnet_diagnostic.RCS1128.severity = error # Use coalesce expression +dotnet_diagnostic.RCS1129.severity = none # Remove redundant field initialization — covered by SST1176 +dotnet_diagnostic.RCS1132.severity = none # Remove redundant overriding member — covered by SST1181 +dotnet_diagnostic.RCS1133.severity = error # Remove redundant Dispose/Close call +dotnet_diagnostic.RCS1134.severity = none # Remove redundant statement — covered by SST1174 +dotnet_diagnostic.RCS1143.severity = error # Simplify coalesce expression +dotnet_diagnostic.RCS1145.severity = error # Remove redundant 'as' operator +dotnet_diagnostic.RCS1146.severity = error # Use conditional access +dotnet_diagnostic.RCS1151.severity = none # Remove redundant cast — covered by SST1175 +dotnet_diagnostic.RCS1171.severity = error # Simplify lazy initialization +dotnet_diagnostic.RCS1173.severity = error # Use coalesce expression instead of 'if' +dotnet_diagnostic.RCS1174.severity = none # Remove redundant async/await — covered by PSH1311 +dotnet_diagnostic.RCS1179.severity = error # Unnecessary assignment +dotnet_diagnostic.RCS1180.severity = error # Inline lazy initialization +dotnet_diagnostic.RCS1188.severity = none # Remove redundant auto-property initialization — covered by SST1176 +dotnet_diagnostic.RCS1192.severity = none # Unnecessary usage of verbatim string literal — covered by SST1184 +dotnet_diagnostic.RCS1199.severity = error # Unnecessary null check +dotnet_diagnostic.RCS1206.severity = error # Use conditional access instead of conditional expression +dotnet_diagnostic.RCS1207.severity = none # Use anonymous function or method group — conflicts with SST2239, which owns the method-group direction +dotnet_diagnostic.RCS1211.severity = none # Remove unnecessary 'else' — covered by SST1464 +dotnet_diagnostic.RCS1212.severity = error # Remove redundant assignment +dotnet_diagnostic.RCS1214.severity = none # Unnecessary interpolated string — covered by SST1183 +dotnet_diagnostic.RCS1216.severity = error # Unnecessary unsafe context +dotnet_diagnostic.RCS1217.severity = none # Convert interpolated string to concatenation — reverses SST2249, which owns the concatenation-to-interpolation direction +dotnet_diagnostic.RCS1218.severity = error # Simplify code branching +dotnet_diagnostic.RCS1220.severity = none # Use pattern matching instead of combination of 'is' and cast — covered by SST2007 +dotnet_diagnostic.RCS1221.severity = error # Use pattern matching instead of combination of 'as' and null check +dotnet_diagnostic.RCS1238.severity = none # Avoid nested ?: operators — covered by SST1147 +dotnet_diagnostic.RCS1244.severity = none # Simplify 'default' expression — covered by SST1188 +dotnet_diagnostic.RCS1249.severity = none # Unnecessary null-forgiving operator — disabled because multi-TFM nullability annotations can differ per platform, leading to false positives +dotnet_diagnostic.RCS1251.severity = error # Remove unnecessary braces from record declaration +dotnet_diagnostic.RCS1259.severity = error # Remove empty syntax (replaces RCS1066) +dotnet_diagnostic.RCS1262.severity = error # Unnecessary raw string literal +dotnet_diagnostic.RCS1265.severity = none # Remove redundant catch block — covered by SST1470 +dotnet_diagnostic.RCS1268.severity = error # Simplify numeric comparison + +# Code quality +dotnet_diagnostic.RCS1013.severity = none # Use predefined type — covered by SST1121 +dotnet_diagnostic.RCS1014.severity = error # Use explicitly/implicitly typed array +dotnet_diagnostic.RCS1015.severity = error # Use nameof operator +dotnet_diagnostic.RCS1016.severity = none # Use block body or expression body — conflicts with SST2219, which owns the expression-bodied accessor direction +dotnet_diagnostic.RCS1020.severity = none # Covered by SST2234 (canonical) +dotnet_diagnostic.RCS1021.severity = error # Convert lambda expression body to expression body +dotnet_diagnostic.RCS1044.severity = none # Remove original exception from throw statement — covered by SST1430 +dotnet_diagnostic.RCS1046.severity = none # Asynchronous method name should end with 'Async' — TUnit test method naming convention doesn't follow the Async suffix — covered by SST1317 +dotnet_diagnostic.RCS1047.severity = error # Non-asynchronous method name should not end with 'Async' +dotnet_diagnostic.RCS1048.severity = none # Use lambda expression instead of anonymous method — covered by SST1130 +dotnet_diagnostic.RCS1050.severity = error # Include/omit parentheses when creating new object +dotnet_diagnostic.RCS1051.severity = none # Add/remove parentheses from condition in conditional operator — conflicts with SST1459, which owns the non-grouping-parenthesis removal direction +dotnet_diagnostic.RCS1056.severity = none # Avoid usage of using alias directive - used to avoid conflicts +dotnet_diagnostic.RCS1059.severity = none # Avoid locking on publicly accessible instance — covered by SST1901 +dotnet_diagnostic.RCS1075.severity = none # Avoid empty catch clause that catches System.Exception — covered by SST1429 +dotnet_diagnostic.RCS1079.severity = error # Throwing of new NotImplementedException +dotnet_diagnostic.RCS1081.severity = error # Split variable declaration +dotnet_diagnostic.RCS1093.severity = error # File contains no code +dotnet_diagnostic.RCS1094.severity = none # Declare using directive on top level — covered by SST1200 +dotnet_diagnostic.RCS1096.severity = none # Use 'HasFlag' method or bitwise operator — covered by PSH1016 +dotnet_diagnostic.RCS1098.severity = none # Constant values should be placed on right side of comparisons — covered by SST1186 +dotnet_diagnostic.RCS1099.severity = none # Default label should be the last label in a switch section — covered by SST1466 +dotnet_diagnostic.RCS1102.severity = none # Make class static — covered by SST1432 +dotnet_diagnostic.RCS1108.severity = error # Add 'static' modifier to all partial class declarations +dotnet_diagnostic.RCS1111.severity = error # Add braces to switch section with multiple statements +dotnet_diagnostic.RCS1113.severity = error # Use 'string.IsNullOrEmpty' method +dotnet_diagnostic.RCS1118.severity = none # Mark local variable as const — covered by PSH1402 +dotnet_diagnostic.RCS1123.severity = none # Add parentheses when necessary — covered by SST1407 +dotnet_diagnostic.RCS1130.severity = none # Bitwise operation on enum without Flags attribute — covered by SST2458 +dotnet_diagnostic.RCS1135.severity = error # Declare enum member with zero value (when enum has FlagsAttribute) +dotnet_diagnostic.RCS1136.severity = none # Merge switch sections with equivalent content — covered by SST2414 +dotnet_diagnostic.RCS1154.severity = error # Sort enum members +dotnet_diagnostic.RCS1155.severity = none # Use StringComparison when comparing strings — covered by PSH1207 +dotnet_diagnostic.RCS1156.severity = none # Use string.Length instead of comparison with empty string — covered by PSH1204 +dotnet_diagnostic.RCS1157.severity = none # Composite enum value contains undefined flag — covered by SST2303 +dotnet_diagnostic.RCS1159.severity = error # Use EventHandler +dotnet_diagnostic.RCS1160.severity = none # Abstract type should not have public constructors — covered by SST1428 +dotnet_diagnostic.RCS1161.severity = none # Enum should declare explicit values - do not need explicit values +dotnet_diagnostic.RCS1162.severity = none # Avoid chain of assignments — covered by SST1187 +dotnet_diagnostic.RCS1166.severity = none # Value type object is never equal to null — covered by SST1469 +dotnet_diagnostic.RCS1168.severity = none # Parameter name differs from base name — covered by SST1318 +dotnet_diagnostic.RCS1169.severity = error # Make field read-only +dotnet_diagnostic.RCS1170.severity = error # Use read-only auto-implemented property +dotnet_diagnostic.RCS1172.severity = none # Use 'is' operator instead of 'as' operator — covered by SST2005 +dotnet_diagnostic.RCS1187.severity = none # Use constant instead of field — covered by PSH1402 +dotnet_diagnostic.RCS1191.severity = error # Declare enum value as combination of names +dotnet_diagnostic.RCS1193.severity = none # Overriding member should not change 'params' modifier — covered by SST2426 +dotnet_diagnostic.RCS1196.severity = error # Call extension method as instance method +dotnet_diagnostic.RCS1200.severity = none # Call 'Enumerable.ThenBy' instead of 'Enumerable.OrderBy' — covered by PSH1108 +dotnet_diagnostic.RCS1201.severity = error # Use method chaining +dotnet_diagnostic.RCS1202.severity = error # Avoid NullReferenceException +dotnet_diagnostic.RCS1204.severity = error # Use EventArgs.Empty +dotnet_diagnostic.RCS1205.severity = error # Order named arguments according to the order of parameters +dotnet_diagnostic.RCS1208.severity = error # Reduce 'if' nesting +dotnet_diagnostic.RCS1209.severity = error # Order type parameter constraints +dotnet_diagnostic.RCS1210.severity = none # Return completed task instead of returning null — covered by PSH1312 +dotnet_diagnostic.RCS1215.severity = error # Expression is always equal to true/false +dotnet_diagnostic.RCS1222.severity = error # Merge preprocessor directives +dotnet_diagnostic.RCS1223.severity = suggestion # Mark publicly visible type with DebuggerDisplay attribute — only data types benefit; the rule is too broad to be an error +dotnet_diagnostic.RCS1224.severity = error # Make method an extension method +dotnet_diagnostic.RCS1225.severity = error # Make class sealed +dotnet_diagnostic.RCS1227.severity = none # Validate arguments correctly — covered by SST2404 +dotnet_diagnostic.RCS1229.severity = error # Use async/await when necessary +dotnet_diagnostic.RCS1231.severity = none # Make parameter ref read-only — covered by PSH1007 +dotnet_diagnostic.RCS1233.severity = none # Use short-circuiting operator — covered by SST1468 +dotnet_diagnostic.RCS1234.severity = error # Duplicate enum value +dotnet_diagnostic.RCS1239.severity = error # Use 'for' statement instead of 'while' statement +dotnet_diagnostic.RCS1240.severity = error # Operator is unnecessary +dotnet_diagnostic.RCS1242.severity = none # Do not pass non-read-only struct by read-only reference — covered by PSH1003 +dotnet_diagnostic.RCS1243.severity = none # Duplicate word in a comment — covered by SST1658 (documentation comments) +dotnet_diagnostic.RCS1247.severity = error # Fix documentation comment tag +dotnet_diagnostic.RCS1248.severity = none # Normalize null check — conflicts with SST1149, which owns the 'is null' pattern direction +dotnet_diagnostic.RCS1250.severity = none # Use implicit/explicit object creation — conflicts with SST2202, which owns the implicit-target-type direction +dotnet_diagnostic.RCS1252.severity = error # Normalize usage of infinite loop +dotnet_diagnostic.RCS1254.severity = error # Normalize format of enum flag value +dotnet_diagnostic.RCS1255.severity = none # Simplify argument null check — conflicts with our ArgumentExceptionHelper helper pattern +dotnet_diagnostic.RCS1257.severity = error # Use enum field explicitly +dotnet_diagnostic.RCS1258.severity = error # Unnecessary enum flag +dotnet_diagnostic.RCS1260.severity = none # Add/remove trailing comma — conflicts with SST1413, which owns the trailing-comma direction +dotnet_diagnostic.RCS1261.severity = none # Resource can be disposed asynchronously — covered by PSH1310 +dotnet_diagnostic.RCS1264.severity = error # Use 'var' or explicit type (replaces RCS1010, RCS1176, RCS1177) +dotnet_diagnostic.RCS1266.severity = none # Use raw string literal — covered by SST2243 +dotnet_diagnostic.RCS1267.severity = error # Use string interpolation instead of 'string.Concat' + +# Performance +dotnet_diagnostic.RCS1077.severity = none # Covered by the PSH1101-PSH1111 family (canonical) +dotnet_diagnostic.RCS1080.severity = none # Covered by PSH1106 (canonical) +dotnet_diagnostic.RCS1112.severity = none # Combine 'Enumerable.Where' method chain — covered by PSH1109 +dotnet_diagnostic.RCS1186.severity = error # Use Regex instance instead of static method +dotnet_diagnostic.RCS1190.severity = none # Join string expressions — conflicts with SST2470, which reports the fused-literal seam this would create +dotnet_diagnostic.RCS1195.severity = error # Use ^ operator +dotnet_diagnostic.RCS1197.severity = none # Optimize StringBuilder.Append/AppendLine call — covered by PSH1203/PSH1214 +dotnet_diagnostic.RCS1198.severity = none # Avoid unnecessary boxing of value type — boxing is unavoidable bridging Rx and IEnumerable +dotnet_diagnostic.RCS1230.severity = none # Unnecessary explicit use of enumerator — covered by SST1467 +dotnet_diagnostic.RCS1235.severity = error # Optimize method call +dotnet_diagnostic.RCS1236.severity = none # Use exception filter — covered by SST2009 +dotnet_diagnostic.RCS1246.severity = none # Use element access — covered by PSH1106 + +# Maintainability +dotnet_diagnostic.RCS1158.severity = none # Static member in generic type should use a type parameter — common factory pattern — covered by SST1431 +dotnet_diagnostic.RCS1163.severity = none # Unused parameter — interface implementations and Rx selectors often have unused parameters +dotnet_diagnostic.RCS1164.severity = none # Unused type parameter - DUPLICATE IDE0060 (UnusedParameter analyzer 210ms; IDE0060 bundled at lower cost) +dotnet_diagnostic.RCS1165.severity = none # Unconstrained type parameter checked for null - we validate all parameters for non-nullable enabled platforms +dotnet_diagnostic.RCS1182.severity = none # Remove redundant base interface — covered by SST1177 +dotnet_diagnostic.RCS1213.severity = none # Remove unused member declaration - DUPLICATE IDE0051 (slower: 230ms vs 75ms) +dotnet_diagnostic.RCS1241.severity = error # Implement non-generic counterpart +dotnet_diagnostic.RCS1256.severity = none # Invalid argument null check — conflicts with our ArgumentExceptionHelper helper pattern + +# Documentation +dotnet_diagnostic.RCS1181.severity = error # Convert comment to documentation comment +dotnet_diagnostic.RCS1189.severity = error # Add or remove region name +dotnet_diagnostic.RCS1226.severity = none # Add paragraph to documentation comment — <para> wrapping is subjective and adds noise +dotnet_diagnostic.RCS1228.severity = error # Unused element in a documentation comment +dotnet_diagnostic.RCS1232.severity = error # Order elements in documentation comment +dotnet_diagnostic.RCS1253.severity = error # Format documentation comment summary +dotnet_diagnostic.RCS1263.severity = none # Invalid reference in a documentation comment + +# Disabled +dotnet_diagnostic.RCS1018.severity = none # Add/remove accessibility modifiers — covered by SA1400 +dotnet_diagnostic.RCS1019.severity = none # Order modifiers — covered by SA1206 / SA1208 +dotnet_diagnostic.RCS1037.severity = none # Remove trailing white-space — covered by SA1028 +dotnet_diagnostic.RCS1052.severity = none # Declare each attribute separately — covered by SA1133 +dotnet_diagnostic.RCS1055.severity = none # Unnecessary semicolon at the end of declaration — covered by SA1106 +dotnet_diagnostic.RCS1060.severity = none # Declare each type in separate file — covered by SA1402 +dotnet_diagnostic.RCS1090.severity = none # Add/remove 'ConfigureAwait(false)' call — covered by CA2007 (also disabled) +dotnet_diagnostic.RCS1110.severity = none # Declare type inside namespace — covered by CA1050 +dotnet_diagnostic.RCS1138.severity = none # Add summary to documentation comment — covered by SA1600 +dotnet_diagnostic.RCS1139.severity = none # Add summary element to documentation comment — covered by SA1604 +dotnet_diagnostic.RCS1140.severity = none # Add exception to documentation comment — covered by SA1614 +dotnet_diagnostic.RCS1141.severity = none # Add 'param' element to documentation comment — covered by SA1611 +dotnet_diagnostic.RCS1142.severity = none # Add 'typeparam' element to documentation comment — covered by SA1618 +dotnet_diagnostic.RCS1175.severity = none # Unused 'this' parameter — covered by CA1822 +dotnet_diagnostic.RCS1194.severity = none # Implement exception constructors — covered by CA1032 +dotnet_diagnostic.RCS1203.severity = none # Use AttributeUsageAttribute — covered by CA1018 + +# Formatting +dotnet_diagnostic.RCS0001.severity = none # Add blank line after embedded statement — covered by StyleCop layout rules +dotnet_diagnostic.RCS0002.severity = none # Add blank line after #region — covered by StyleCop SA1517 family +dotnet_diagnostic.RCS0003.severity = none # Add blank line after using directive list — covered by SA1516 +dotnet_diagnostic.RCS0005.severity = none # Add blank line before #endregion — covered by StyleCop layout rules +dotnet_diagnostic.RCS0006.severity = none # Add blank line before using directive list — covered by SA1517 +dotnet_diagnostic.RCS0007.severity = none # Add blank line between accessors — covered by SA1513 +dotnet_diagnostic.RCS0008.severity = none # Add blank line between closing brace and next statement — covered by SA1513 +dotnet_diagnostic.RCS0009.severity = none # Add blank line between declaration and documentation comment — covered by SA1514 +dotnet_diagnostic.RCS0010.severity = none # Add blank line between declarations — covered by SA1516 +dotnet_diagnostic.RCS0011.severity = none # Add/remove blank line between single-line accessors — StyleCop already governs this +dotnet_diagnostic.RCS0012.severity = none # Add blank line between single-line declarations — covered by SA1516 +dotnet_diagnostic.RCS0013.severity = none # Add blank line between single-line declarations of different kind — covered by SA1516 +dotnet_diagnostic.RCS0015.severity = none # Add/remove blank line between using directives — covered by SA1209 / SA1210 +dotnet_diagnostic.RCS0016.severity = none # Put attribute list on its own line — StyleCop SA1133 governs attribute lists +dotnet_diagnostic.RCS0020.severity = none # Format accessor's braces — covered by SA1500 +dotnet_diagnostic.RCS0021.severity = none # Format block's braces — covered by SA1500 +dotnet_diagnostic.RCS0023.severity = none # Format type declaration's braces — covered by SA1500 +dotnet_diagnostic.RCS0024.severity = none # Add new line after switch label — covered by SA1003 +dotnet_diagnostic.RCS0025.severity = none # Put full accessor on its own line — covered by SA1502 +dotnet_diagnostic.RCS0027.severity = none # Place new line after/before binary operator — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0028.severity = none # Place new line after/before '?:' operator — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0029.severity = none # Put constructor initializer on its own line — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0030.severity = none # Put embedded statement on its own line — covered by SA1503 +dotnet_diagnostic.RCS0031.severity = none # Put enum member on its own line — covered by SA1136 +dotnet_diagnostic.RCS0032.severity = none # Place new line after/before arrow token — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0033.severity = none # Put statement on its own line — covered by SA1505 family +dotnet_diagnostic.RCS0034.severity = none # Put type parameter constraint on its own line — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0036.severity = none # Remove blank line between single-line declarations of same kind — covered by SA1516 +dotnet_diagnostic.RCS0039.severity = none # Remove new line before base list — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0041.severity = none # Remove new line between 'if' keyword and 'else' keyword — StyleCop layout rules cover this +dotnet_diagnostic.RCS0042.severity = none # Put auto-accessors on a single line — formatting preference, not enforced +dotnet_diagnostic.RCS0044.severity = none # Use carriage return + linefeed as new line — handled by .gitattributes / editorconfig end_of_line +dotnet_diagnostic.RCS0045.severity = none # Use linefeed as new line — handled by .gitattributes / editorconfig end_of_line +dotnet_diagnostic.RCS0046.severity = none # Use spaces instead of tab — handled by editorconfig indent_style +dotnet_diagnostic.RCS0048.severity = none # Put initializer on a single line — formatting preference, not enforced +dotnet_diagnostic.RCS0049.severity = none # Add blank line after top comment — covered by SA1517 +dotnet_diagnostic.RCS0050.severity = none # Add blank line before top declaration — covered by SA1517 +dotnet_diagnostic.RCS0051.severity = none # Add/remove new line before 'while' in 'do' statement — formatting preference +dotnet_diagnostic.RCS0052.severity = none # Place new line after/before equals token — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0053.severity = none # Fix formatting of a list — formatting preference +dotnet_diagnostic.RCS0054.severity = none # Fix formatting of a call chain — formatting preference +dotnet_diagnostic.RCS0055.severity = none # Fix formatting of a binary expression chain — formatting preference +dotnet_diagnostic.RCS0056.severity = none # A line is too long — line-length not enforced +dotnet_diagnostic.RCS0057.severity = none # Normalize whitespace at the beginning of a file — handled by editorconfig +dotnet_diagnostic.RCS0058.severity = none # Normalize whitespace at the end of a file — covered by SST1518 +dotnet_diagnostic.RCS0059.severity = none # Place new line after/before null-conditional operator — StyleSharp wrapping rules cover this +dotnet_diagnostic.RCS0060.severity = none # Add/remove line after file scoped namespace declaration — formatting preference +dotnet_diagnostic.RCS0061.severity = none # Add/remove blank line between switch sections — formatting preference +dotnet_diagnostic.RCS0062.severity = none # Put expression body on its own line — formatting preference +dotnet_diagnostic.RCS0063.severity = none # Remove unnecessary blank line — covered by SST1505 / SST1507 + +################### +# StyleSharp Analyzers (SST) +################### +file_header_template = Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.\nReactiveUI Association Incorporated licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for full license information. +stylesharp.summary_single_line_max_length = 120 + +# SonarAnalyzer's S4022 only ever flagged storage narrower than int; uint, long and ulong +# passed silently. Match that, so retiring S4022 for SST2313 does not fail a build on an +# enum that was deliberately widened. +stylesharp.SST2313.allowed_enum_storage = int, uint, long, ulong + +# Documentation coverage scope (SST1600/SST1601/SST1602/SST1654). +# Require documentation on every element, including private members. +stylesharp.document_exposed_elements = true +stylesharp.document_internal_elements = true +stylesharp.document_private_elements = true +stylesharp.document_private_fields = true +stylesharp.document_interfaces = all +stylesharp.SST1305.allowed_hungarian_prefixes = rx +stylesharp.instance_member_qualification = omit_this +stylesharp.max_cyclomatic_complexity = 10 +stylesharp.max_cognitive_complexity = 15 +stylesharp.max_property_cognitive_complexity = 3 +# SST1484 also reports a field that shadows one inherited from a base type. +stylesharp.SST1484.check_base_types = true +stylesharp.max_line_length = 200 # SST1521 (characters; keeps the limit this repo has always enforced) +stylesharp.max_file_lines = 1000 # SST1522 (code lines; blank lines and comments do not count) +# stylesharp.max_member_lines = 60 # SST1523 (code lines) +# stylesharp.max_switch_section_lines = 20 # SST1524 (code lines) +# stylesharp.include_internal = true # SST1499 (set false to report only fields visible outside the assembly) +# stylesharp.require_parameterless = true # SST1488 (set false where every exception must carry a message) +# stylesharp.include_non_public_types = true # SST1488 (set false to check only externally visible exceptions) +# Spacing +dotnet_diagnostic.SST1000.severity = error # A control-flow keyword is not followed by a space +dotnet_diagnostic.SST1001.severity = error # A comma is spaced incorrectly +dotnet_diagnostic.SST1002.severity = error # A semicolon is spaced incorrectly +dotnet_diagnostic.SST1003.severity = error # A binary operator is not surrounded by spaces +dotnet_diagnostic.SST1004.severity = error # A documentation comment line does not begin with a single space after the '///' +dotnet_diagnostic.SST1005.severity = error # A single-line comment does not begin with a single space +dotnet_diagnostic.SST1006.severity = error # A preprocessor keyword is preceded by a space after the '#' +dotnet_diagnostic.SST1007.severity = error # An operator keyword is not followed by a space +dotnet_diagnostic.SST1008.severity = error # An opening parenthesis is followed by a space +dotnet_diagnostic.SST1009.severity = error # A closing parenthesis is preceded by a space +dotnet_diagnostic.SST1010.severity = none # An opening square bracket has adjacent whitespace — conflicts with modern collection expressions +dotnet_diagnostic.SST1011.severity = error # A closing square bracket is preceded by a space +dotnet_diagnostic.SST1012.severity = error # An opening brace is not followed by a space on a single line +dotnet_diagnostic.SST1013.severity = error # A closing brace is not preceded by a space on a single line +dotnet_diagnostic.SST1014.severity = error # An opening generic bracket is preceded or followed by a space +dotnet_diagnostic.SST1015.severity = error # A closing generic bracket is preceded by a space +dotnet_diagnostic.SST1016.severity = error # An opening attribute bracket is followed by a space +dotnet_diagnostic.SST1017.severity = error # A closing attribute bracket is preceded by a space +dotnet_diagnostic.SST1018.severity = error # A nullable type symbol is preceded by a space +dotnet_diagnostic.SST1019.severity = error # A member access symbol is surrounded by spaces +dotnet_diagnostic.SST1020.severity = error # An increment or decrement symbol is separated from its operand by a space +dotnet_diagnostic.SST1021.severity = error # A unary negative sign is followed by a space +dotnet_diagnostic.SST1022.severity = error # A unary positive sign is followed by a space +dotnet_diagnostic.SST1023.severity = error # A dereference or address-of symbol is followed by a space +dotnet_diagnostic.SST1024.severity = error # A colon is spaced incorrectly for its context +dotnet_diagnostic.SST1025.severity = error # Two or more whitespace characters appear in a row within a line +dotnet_diagnostic.SST1026.severity = error # A space follows 'new' or 'stackalloc' in an implicit array creation +dotnet_diagnostic.SST1027.severity = error # A tab character is used where the project standardises on spaces +dotnet_diagnostic.SST1028.severity = error # A line ends with trailing whitespace + +# Readability and maintainability +dotnet_diagnostic.SST1100.severity = error # A base. prefix is used where the type does not override the member +dotnet_diagnostic.SST1101.severity = none # see docs/rules/SST1101.md +dotnet_diagnostic.SST1102.severity = error # A query clause is separated from the previous clause by a blank line +dotnet_diagnostic.SST1103.severity = error # Query clauses mix single-line and multi-line layout +dotnet_diagnostic.SST1104.severity = error # A query clause shares the last line of a multi-line previous clause +dotnet_diagnostic.SST1105.severity = error # A multi-line query clause does not begin on its own line +dotnet_diagnostic.SST1106.severity = error # A statement is empty (a stray semicolon) +dotnet_diagnostic.SST1107.severity = error # More than one statement shares a line +dotnet_diagnostic.SST1110.severity = error # An opening parenthesis or bracket does not sit on the line of the preceding code +dotnet_diagnostic.SST1111.severity = error # A closing parenthesis or bracket does not sit on the last parameter's line +dotnet_diagnostic.SST1112.severity = error # An empty parameter list's closing parenthesis is on a different line +dotnet_diagnostic.SST1113.severity = error # A comma does not sit on the previous parameter's line +dotnet_diagnostic.SST1114.severity = error # A blank line separates the declaration from its parameter list +dotnet_diagnostic.SST1115.severity = error # A blank line separates a parameter from the preceding comma +dotnet_diagnostic.SST1116.severity = error # A qualified name can be shortened without changing the symbol it binds to +dotnet_diagnostic.SST1117.severity = error # Instance member access follows the configured this. qualification style +dotnet_diagnostic.SST1118.severity = none # A parameter should not span multiple lines +dotnet_diagnostic.SST1119.severity = error # A numeric literal groups its digit separators irregularly +dotnet_diagnostic.SST1120.severity = error # A comment contains no text +dotnet_diagnostic.SST1121.severity = error # A framework type name is used instead of its built-in alias (opt-in rule, enabled here) +dotnet_diagnostic.SST1122.severity = error # An empty string literal is used instead of string.Empty +dotnet_diagnostic.SST1123.severity = error # A #region is placed inside a code element body +dotnet_diagnostic.SST1124.severity = error # A #region directive is used +dotnet_diagnostic.SST1125.severity = error # A Nullable type is written in long form instead of the T? shorthand +dotnet_diagnostic.SST1127.severity = error # A generic type constraint shares a line with the declaration or another constraint +dotnet_diagnostic.SST1128.severity = error # A constructor initializer shares a line with the constructor signature +dotnet_diagnostic.SST1129.severity = error # A value type is created with a parameterless constructor call instead of default +dotnet_diagnostic.SST1130.severity = error # An anonymous delegate is used where a lambda expression is clearer +dotnet_diagnostic.SST1131.severity = error # A comparison places the constant on the left ("yoda" condition) +dotnet_diagnostic.SST1132.severity = error # Several fields are declared in a single statement +dotnet_diagnostic.SST1133.severity = error # Several attributes share one bracket list +dotnet_diagnostic.SST1134.severity = error # An attribute shares a line with another attribute or the element +dotnet_diagnostic.SST1135.severity = error # A using directive names a namespace or type that is not fully qualified +dotnet_diagnostic.SST1136.severity = error # Several enum members share a line +dotnet_diagnostic.SST1137.severity = error # Sibling elements are indented differently from one another +dotnet_diagnostic.SST1138.severity = error # A free-standing block declares nothing +dotnet_diagnostic.SST1139.severity = error # A numeric literal is cast where a literal suffix would express the type +dotnet_diagnostic.SST1140.severity = error # Wrapped conditional operators should start indented continuation lines +dotnet_diagnostic.SST1141.severity = error # An explicit ValueTuple<...> is used where tuple syntax would do +dotnet_diagnostic.SST1142.severity = error # A tuple element is accessed by ItemN where it has a name +dotnet_diagnostic.SST1143.severity = error # A boolean expression is compared to a true/false literal +dotnet_diagnostic.SST1144.severity = error # Combine case labels with an or-pattern +dotnet_diagnostic.SST1145.severity = error # A wrapped conditional expression places ?/: inconsistently +dotnet_diagnostic.SST1146.severity = error # An if statement follows a closing brace on the same line +dotnet_diagnostic.SST1147.severity = error # Do not nest conditional operators +dotnet_diagnostic.SST1148.severity = error # Commented-out code should be removed +dotnet_diagnostic.SST1149.severity = error # Prefer the 'is null' pattern for null checks +dotnet_diagnostic.SST1150.severity = error # Require each constructor parameter on a unique line +dotnet_diagnostic.SST1151.severity = error # Require each method parameter on a unique line +dotnet_diagnostic.SST1152.severity = error # Require each delegate parameter on a unique line +dotnet_diagnostic.SST1153.severity = error # Require each indexer parameter on a unique line +dotnet_diagnostic.SST1154.severity = error # Require each invocation argument on a unique line +dotnet_diagnostic.SST1155.severity = error # Require each object creation argument on a unique line +dotnet_diagnostic.SST1156.severity = error # Require each element access argument on a unique line +dotnet_diagnostic.SST1157.severity = error # Require each attribute argument on a unique line +dotnet_diagnostic.SST1158.severity = error # Require each anonymous method parameter on a unique line +dotnet_diagnostic.SST1159.severity = error # Require each parenthesized lambda parameter on a unique line +dotnet_diagnostic.SST1160.severity = error # Require each record primary-constructor parameter on a unique line +dotnet_diagnostic.SST1161.severity = error # Require each class primary-constructor parameter on a unique line +dotnet_diagnostic.SST1162.severity = error # Require each struct primary-constructor parameter on a unique line +dotnet_diagnostic.SST1163.severity = error # Require each target-typed object creation argument on a unique line +dotnet_diagnostic.SST1164.severity = error # Require each constructor initializer argument on a unique line +dotnet_diagnostic.SST1165.severity = error # Require each primary-constructor base argument on a unique line +dotnet_diagnostic.SST1166.severity = error # Require each local-function parameter on a unique line +dotnet_diagnostic.SST1167.severity = error # Require each operator parameter on a unique line +dotnet_diagnostic.SST1168.severity = error # Require each conversion-operator parameter on a unique line +dotnet_diagnostic.SST1169.severity = error # Require each type parameter on a unique line +dotnet_diagnostic.SST1170.severity = error # Require each type argument on a unique line +dotnet_diagnostic.SST1171.severity = error # Require each function-pointer parameter on a unique line +dotnet_diagnostic.SST1172.severity = error # Negated comparisons should use the opposite operator +dotnet_diagnostic.SST1173.severity = error # Redundant anonymous-type member names should be omitted +dotnet_diagnostic.SST1174.severity = error # Redundant jump statements should be removed +dotnet_diagnostic.SST1175.severity = error # Unnecessary casts should be removed +dotnet_diagnostic.SST1176.severity = error # Members should not be initialized to their default value +dotnet_diagnostic.SST1177.severity = error # Redundant base types should be removed +dotnet_diagnostic.SST1178.severity = error # Redundant base constructor calls should be removed +dotnet_diagnostic.SST1179.severity = error # Redundant default switch sections should be removed +dotnet_diagnostic.SST1180.severity = error # Empty else clauses should be removed +dotnet_diagnostic.SST1181.severity = error # Redundant overriding members should be removed +dotnet_diagnostic.SST1182.severity = error # Conditional expressions should not just return boolean literals +dotnet_diagnostic.SST1183.severity = error # Interpolated strings without interpolations should be plain strings +dotnet_diagnostic.SST1184.severity = error # Verbatim string literals without escapes should be regular strings +dotnet_diagnostic.SST1185.severity = error # Use a compound assignment operator +dotnet_diagnostic.SST1186.severity = error # A literal should be on the right side of a comparison +dotnet_diagnostic.SST1187.severity = error # Assignments should not be chained +dotnet_diagnostic.SST1188.severity = error # Use the 'default' literal instead of 'default(T)' +dotnet_diagnostic.SST1189.severity = error # Variables should not be self-assigned +dotnet_diagnostic.SST1190.severity = error # Doubled negation operators should be removed +dotnet_diagnostic.SST1191.severity = error # Long numeric literals should use digit separators +dotnet_diagnostic.SST1192.severity = none # Control characters in string literals should be escaped +dotnet_diagnostic.SST1193.severity = error # Keep initial member values with construction +dotnet_diagnostic.SST1194.severity = error # Keep initial collection values with construction +dotnet_diagnostic.SST1195.severity = error # Write null fallback with ?? +dotnet_diagnostic.SST1196.severity = error # Write null-guarded access with ?. +dotnet_diagnostic.SST1197.severity = error # Collapse return-only branches into one return +dotnet_diagnostic.SST1198.severity = error # Collapse assignment-only branches into one assignment +dotnet_diagnostic.SST1199.severity = error # Prefer compile-time type names +# Ordering +dotnet_diagnostic.SST1200.severity = error # Using directives should be placed outside the namespace +dotnet_diagnostic.SST1201.severity = error # Members should be ordered by kind +dotnet_diagnostic.SST1202.severity = error # Members should be ordered by accessibility +dotnet_diagnostic.SST1203.severity = error # Constants should appear before fields +dotnet_diagnostic.SST1204.severity = error # Static members should appear before instance members +dotnet_diagnostic.SST1205.severity = error # Partial elements should declare an access modifier +dotnet_diagnostic.SST1206.severity = error # Declaration keywords should follow the standard order +dotnet_diagnostic.SST1207.severity = error # Place protected before internal +dotnet_diagnostic.SST1208.severity = error # System using directives should appear before other usings +dotnet_diagnostic.SST1209.severity = error # Using alias directives should appear after other usings +dotnet_diagnostic.SST1210.severity = error # Regular using directives should be ordered alphabetically +dotnet_diagnostic.SST1211.severity = error # Using alias directives should be ordered alphabetically by alias +dotnet_diagnostic.SST1212.severity = error # Property accessors should be ordered with get first +dotnet_diagnostic.SST1213.severity = error # Event accessors should be ordered with add first +dotnet_diagnostic.SST1214.severity = error # Static readonly fields should appear before static non-readonly fields +dotnet_diagnostic.SST1215.severity = error # Instance readonly fields should appear before instance non-readonly fields +dotnet_diagnostic.SST1216.severity = error # Using static directives should be placed after regular usings and before aliases +dotnet_diagnostic.SST1217.severity = error # Using static directives should be ordered alphabetically +dotnet_diagnostic.SST1218.severity = error # Other members separate a method's overloads +dotnet_diagnostic.SST1219.severity = error # A switch default clause is not placed last +dotnet_diagnostic.SST1220.severity = error # An all-named argument list is in a different order than the parameters. Code fix reorders it to declaration order. Info. +dotnet_diagnostic.SST1221.severity = error # `where` constraint clauses are not ordered to match the type-parameter list. Code fix reorders them. Info. + +# Naming +dotnet_diagnostic.SST1300.severity = none # Types and members should be PascalCase — naming duplicates existing analyzers +dotnet_diagnostic.SST1302.severity = error # Interface names should begin with I +dotnet_diagnostic.SST1303.severity = error # Const names should be PascalCase +dotnet_diagnostic.SST1304.severity = error # Non-private readonly fields should be PascalCase +dotnet_diagnostic.SST1305.severity = error # Field names should not use Hungarian notation +dotnet_diagnostic.SST1306.severity = none # Field names should begin with a lower-case letter — private fields use _camelCase here +dotnet_diagnostic.SST1307.severity = error # Accessible fields should be PascalCase +dotnet_diagnostic.SST1308.severity = none # Field names should not be prefixed with m_ or s_ — too broad for existing conventions +dotnet_diagnostic.SST1309.severity = error # Private fields should be _camelCase +dotnet_diagnostic.SST1310.severity = none # Field names should not contain underscores — conflicts with _camelCase +dotnet_diagnostic.SST1311.severity = none # Static readonly fields should be PascalCase — we use _camelCase for private static readonly too +dotnet_diagnostic.SST1312.severity = error # Local variables should be camelCase +dotnet_diagnostic.SST1313.severity = error # Parameters should be camelCase +dotnet_diagnostic.SST1314.severity = error # Type parameters should begin with T +dotnet_diagnostic.SST1315.severity = error # Union member names should match the configured casing +dotnet_diagnostic.SST1316.severity = none # Tuple element names should use the configured casing — tuple naming is not enforced here +dotnet_diagnostic.SST1317.severity = none # Asynchronous method names should end with 'Async' — conflicts with this project's Rx-compatibility and naming mechanism +dotnet_diagnostic.SST1318.severity = error # Overriding parameter names should match the base declaration +dotnet_diagnostic.SST1319.severity = error # An enumeration's type name is not PascalCase +dotnet_diagnostic.SST1320.severity = error # A parameter name matches its method's name +dotnet_diagnostic.SST1321.severity = none # Public APIs intentionally use Async to describe asynchronous observable behavior without returning an awaitable. + +# Maintainability +dotnet_diagnostic.SST1400.severity = error # An element does not declare an access modifier +dotnet_diagnostic.SST1401.severity = error # A non-private, non-constant field is exposed +dotnet_diagnostic.SST1402.severity = error # A file declares more than one top-level type +dotnet_diagnostic.SST1403.severity = error # A file declares more than one namespace +dotnet_diagnostic.SST1404.severity = error # A code-analysis suppression has no justification +dotnet_diagnostic.SST1405.severity = error # A Debug.Assert call provides no message +dotnet_diagnostic.SST1406.severity = error # A Debug.Fail call provides no message +dotnet_diagnostic.SST1407.severity = error # Mixed-precedence arithmetic is not parenthesized +dotnet_diagnostic.SST1408.severity = error # Mixed conditional operators are not parenthesized +dotnet_diagnostic.SST1410.severity = error # An anonymous method has an empty parameter list +dotnet_diagnostic.SST1411.severity = error # An attribute uses an empty argument list +dotnet_diagnostic.SST1412.severity = none # Store files as UTF-8 with a byte order mark — conflicts with SST1450 (UTF-8 without BOM) +dotnet_diagnostic.SST1413.severity = none # A multi-line initializer omits the trailing comma — trailing commas are not required here +dotnet_diagnostic.SST1414.severity = error # A tuple type in a member signature has an unnamed element +dotnet_diagnostic.SST1415.severity = error # An argument-exception constructor uses a string literal where nameof would track renames +dotnet_diagnostic.SST1416.severity = none # Do not declare public members in a non-public type +dotnet_diagnostic.SST1417.severity = suggestion # Namespace should match the folder structure +dotnet_diagnostic.SST1418.severity = error # Declare precedence when mixing the null-coalescing operator +dotnet_diagnostic.SST1419.severity = error # Remove redundant modifiers +dotnet_diagnostic.SST1420.severity = error # Trivial properties should be auto-implemented +dotnet_diagnostic.SST1421.severity = error # Write-only properties should not be used +dotnet_diagnostic.SST1422.severity = error # Private fields used only as locals should be local variables +dotnet_diagnostic.SST1423.severity = error # Switch statements should not have too many sections +dotnet_diagnostic.SST1424.severity = error # Fields that are never reassigned should be readonly +dotnet_diagnostic.SST1425.severity = error # Do not reassign captured primary-constructor parameters +dotnet_diagnostic.SST1426.severity = error # Use [SuppressMessage] instead of #pragma warning disable +dotnet_diagnostic.SST1427.severity = error # Protected members of sealed types should not be used +dotnet_diagnostic.SST1428.severity = error # Abstract types should not declare public constructors +dotnet_diagnostic.SST1429.severity = error # Empty catch clauses should not swallow the base exception +dotnet_diagnostic.SST1430.severity = error # Rethrow with 'throw;' to preserve the stack trace +dotnet_diagnostic.SST1431.severity = error # Static members of a generic type should use a type parameter +dotnet_diagnostic.SST1432.severity = error # Classes with only static members should be static +dotnet_diagnostic.SST1433.severity = error # Redundant constructors should be removed +dotnet_diagnostic.SST1434.severity = error # see docs/rules/SST1434.md +dotnet_diagnostic.SST1435.severity = error # Empty namespace declarations should be removed +dotnet_diagnostic.SST1436.severity = error # Empty types should not be declared +dotnet_diagnostic.SST1437.severity = error # Empty interfaces should not be declared +dotnet_diagnostic.SST1438.severity = error # Methods should not be empty +dotnet_diagnostic.SST1439.severity = error # Nested code blocks should not be left empty +dotnet_diagnostic.SST1440.severity = error # Private members with no local use should be removed +dotnet_diagnostic.SST1441.severity = error # Private fields assigned but never read should be removed +dotnet_diagnostic.SST1442.severity = error # A function has too many direct branch points +dotnet_diagnostic.SST1443.severity = error # A function has too much nested control flow +dotnet_diagnostic.SST1444.severity = error # A loop cannot naturally reach a second iteration +dotnet_diagnostic.SST1445.severity = error # A using directive is unnecessary +dotnet_diagnostic.SST1446.severity = error # An inheritance chain is deeper than the configured maximum +dotnet_diagnostic.SST1447.severity = error # An equality override delegates to object reference semantics +dotnet_diagnostic.SST1448.severity = error # An argument is passed explicitly to a caller-info parameter +dotnet_diagnostic.SST1449.severity = error # Code writes directly to the console +dotnet_diagnostic.SST1450.severity = error # Store files as UTF-8 without a byte order mark +dotnet_diagnostic.SST1451.severity = error # A DateTime is created without a DateTimeKind +dotnet_diagnostic.SST1452.severity = error # A generic type parameter is never used +dotnet_diagnostic.SST1453.severity = error # Statements after an unconditional exit should be removed +dotnet_diagnostic.SST1454.severity = error # Composite format placeholders should match the supplied arguments +dotnet_diagnostic.SST1455.severity = error # Unsafe modifiers should be used only when unsafe syntax is present +dotnet_diagnostic.SST1456.severity = error # Readonly fields should not store mutable source-defined structs +dotnet_diagnostic.SST1457.severity = error # Global suppressions should point at real declarations +dotnet_diagnostic.SST1458.severity = error # Global suppression targets should use declaration ids directly +dotnet_diagnostic.SST1459.severity = error # Grouping parentheses should be removed when the parent syntax already isolates the expression +dotnet_diagnostic.SST1460.severity = error # Non-mutating struct members should be readonly +dotnet_diagnostic.SST1461.severity = error # Private parameters that are never read should be removed +dotnet_diagnostic.SST1462.severity = error # Suppressions for diagnostics already disabled by config should be removed +dotnet_diagnostic.SST1463.severity = error # Symbol-name strings should use nameof +dotnet_diagnostic.SST1464.severity = error # An else clause follows a branch that always jumps and can be unwrapped +dotnet_diagnostic.SST1465.severity = error # An else block that only wraps an if can collapse to else-if +dotnet_diagnostic.SST1466.severity = error # A case label sharing a section with default is redundant +dotnet_diagnostic.SST1467.severity = error # A hand-driven enumerator loop can use foreach +dotnet_diagnostic.SST1468.severity = error # Boolean logic should use the short-circuiting && and || operators +dotnet_diagnostic.SST1469.severity = error # A non-nullable value type is compared to null +dotnet_diagnostic.SST1470.severity = error # A trailing catch clause that only rethrows should be removed +dotnet_diagnostic.SST1471.severity = error # Magic numbers should be named constants +dotnet_diagnostic.SST1472.severity = error # Signatures should not declare too many parameters +dotnet_diagnostic.SST1473.severity = error # A floating-point value is compared for exact equality (zero comparison allowed by default) +dotnet_diagnostic.SST1474.severity = error # Both sides of an operator are the same expression +dotnet_diagnostic.SST1475.severity = error # A condition repeats an earlier one in the chain, so its branch cannot run +dotnet_diagnostic.SST1476.severity = error # Every branch of a conditional has the same body +dotnet_diagnostic.SST1477.severity = error # An integer division is widened to floating point after it has already truncated +dotnet_diagnostic.SST1478.severity = error # A shift count is zero, negative, or at least the operand's width +dotnet_diagnostic.SST1479.severity = error # A count or length is compared against a value it can never take +dotnet_diagnostic.SST1480.severity = error # An exception is constructed and then discarded +dotnet_diagnostic.SST1481.severity = error # A bitwise operation has a constant operand that makes it pointless +dotnet_diagnostic.SST1482.severity = error # GetHashCode reads mutable state +dotnet_diagnostic.SST1483.severity = error # A constructor calls an overridable member +dotnet_diagnostic.SST1484.severity = error # A declaration shadows an outer field or property (inherited fields included) +dotnet_diagnostic.SST1485.severity = error # A member that must not throw throws +dotnet_diagnostic.SST1486.severity = error # The same string literal is repeated instead of being named +dotnet_diagnostic.SST1487.severity = error # A collection element is assigned twice with nothing reading it in between +dotnet_diagnostic.SST1488.severity = error # An exception type does not declare the standard constructors (parameterless waivable) +dotnet_diagnostic.SST1489.severity = error # An exception type carries serialization members the target framework has obsoleted +dotnet_diagnostic.SST1490.severity = error # A base list names an interface the rest of the list already implies +dotnet_diagnostic.SST1491.severity = error # A modifier restates the declaration's default +dotnet_diagnostic.SST1492.severity = error # A value is tested against what it is then assigned, so the guard decides nothing +dotnet_diagnostic.SST1493.severity = error # A method's whole body is a constant +dotnet_diagnostic.SST1494.severity = error # A trailing argument repeats the parameter's default +dotnet_diagnostic.SST1495.severity = error # '==' compares references on a type that overrides Equals, so the two disagree +dotnet_diagnostic.SST1496.severity = error # An abstract type declares nothing abstract +dotnet_diagnostic.SST1497.severity = error # A local is declared and never read +dotnet_diagnostic.SST1498.severity = error # Only a nested type uses a private member +dotnet_diagnostic.SST1499.severity = error # A static field visible outside its type can still be changed (internal fields included by default) + +# Layout +dotnet_diagnostic.SST1500.severity = error # A brace in a multi-line construct shares its line with other code +dotnet_diagnostic.SST1501.severity = error # A statement block is collapsed onto a single line +dotnet_diagnostic.SST1502.severity = error # An element body is collapsed onto a single line +dotnet_diagnostic.SST1503.severity = none # A control-flow statement omits the braces around its child statement — duplicate with existing brace preferences +dotnet_diagnostic.SST1504.severity = error # The accessors of a property/event mix single-line and multi-line forms +dotnet_diagnostic.SST1505.severity = error # An opening brace is followed by a blank line +dotnet_diagnostic.SST1506.severity = error # An element documentation header is followed by a blank line +dotnet_diagnostic.SST1507.severity = error # Two or more blank lines appear in a row +dotnet_diagnostic.SST1508.severity = error # A closing brace is preceded by a blank line +dotnet_diagnostic.SST1509.severity = error # An opening brace is preceded by a blank line +dotnet_diagnostic.SST1510.severity = error # A chained block such as else/catch/finally is preceded by a blank line +dotnet_diagnostic.SST1511.severity = error # The while footer of a do/while loop is preceded by a blank line +dotnet_diagnostic.SST1512.severity = error # A single-line comment is followed by a blank line +dotnet_diagnostic.SST1513.severity = error # A closing brace is not followed by a blank line +dotnet_diagnostic.SST1514.severity = error # An element documentation header is not preceded by a blank line +dotnet_diagnostic.SST1515.severity = error # A single-line comment is not preceded by a blank line +dotnet_diagnostic.SST1516.severity = error # Adjacent members or namespace elements are not separated by a blank line +dotnet_diagnostic.SST1517.severity = error # The file begins with one or more blank lines +dotnet_diagnostic.SST1518.severity = error # The file does not end with exactly one newline +dotnet_diagnostic.SST1519.severity = error # A multi-line child statement of a control-flow keyword omits its braces +dotnet_diagnostic.SST1520.severity = error # The clauses of an if/else chain use braces inconsistently +dotnet_diagnostic.SST1521.severity = error # A line is longer than the configured maximum (default 120 characters) +dotnet_diagnostic.SST1522.severity = error # A file has more code lines than the configured maximum (default 500) +dotnet_diagnostic.SST1523.severity = error # A member has more code lines than the configured maximum (default 60) +dotnet_diagnostic.SST1524.severity = error # A switch section has more code lines than the configured maximum (default 20) +dotnet_diagnostic.SST1525.severity = error # A multi-statement `switch` section has no braces; the braces-on policy extends to switch sections. Code fix wraps it. +dotnet_diagnostic.SST1526.severity = error # A wrapped binary expression places the operator inconsistently. Configurable (`before`/`after`, default before). Opt-in. +dotnet_diagnostic.SST1527.severity = error # The `=>` of an expression-bodied member wraps inconsistently. Configurable. Opt-in. +dotnet_diagnostic.SST1528.severity = error # The `=` of a wrapped initializer wraps inconsistently. Configurable. Opt-in. +dotnet_diagnostic.SST1529.severity = error # A wrapped `?.`/`.` call chain places the break inconsistently. Configurable. Opt-in. +dotnet_diagnostic.SST1530.severity = error # A newline sits between a type declaration and its base list. Code fix pulls the base list onto the declaration line. Opt-in. +dotnet_diagnostic.SST1531.severity = error # A short object initializer is split across lines. Code fix collapses it when it fits. Opt-in. +dotnet_diagnostic.SST1532.severity = error # A file mixes line endings. Configurable (`lf`/`crlf`, default lf). Opt-in. +dotnet_diagnostic.SST1533.severity = error # A source file contains no code. Opt-in. + +# Documentation +dotnet_diagnostic.SST1600.severity = error # Externally visible members should be documented +dotnet_diagnostic.SST1601.severity = error # Partial elements should be documented +dotnet_diagnostic.SST1602.severity = error # Enumeration members should be documented +dotnet_diagnostic.SST1604.severity = error # Element documentation should contain a summary +dotnet_diagnostic.SST1605.severity = error # Partial element documentation should have a summary +dotnet_diagnostic.SST1606.severity = error # The summary should have text +dotnet_diagnostic.SST1607.severity = error # Partial element summary should have text +dotnet_diagnostic.SST1608.severity = error # Documentation should not use the default placeholder summary +dotnet_diagnostic.SST1609.severity = none # Property documentation should have a value +dotnet_diagnostic.SST1610.severity = error # Property value documentation should have text +dotnet_diagnostic.SST1611.severity = error # Parameters should be documented +dotnet_diagnostic.SST1612.severity = error # Parameter documentation should match the parameters +dotnet_diagnostic.SST1613.severity = error # Parameter documentation should declare a name +dotnet_diagnostic.SST1614.severity = error # Parameter documentation should have text +dotnet_diagnostic.SST1615.severity = error # The return value should be documented + +dotnet_diagnostic.SST1616.severity = error # Return value documentation should have text +dotnet_diagnostic.SST1617.severity = error # A void return value should not be documented +dotnet_diagnostic.SST1618.severity = error # Generic type parameters should be documented +dotnet_diagnostic.SST1619.severity = error # Partial generic type parameters should be documented +dotnet_diagnostic.SST1620.severity = error # Type parameter documentation should match the type parameters +dotnet_diagnostic.SST1621.severity = error # Type parameter documentation should declare a name +dotnet_diagnostic.SST1622.severity = error # Type parameter documentation should have text +dotnet_diagnostic.SST1623.severity = error # Property summaries should describe their accessors +dotnet_diagnostic.SST1624.severity = error # Property summary should omit a restricted set accessor +dotnet_diagnostic.SST1625.severity = error # Element documentation should not be copy-pasted +dotnet_diagnostic.SST1626.severity = error # A documentation-style comment is used where it does not document an element +dotnet_diagnostic.SST1627.severity = error # Documentation text should not be empty +dotnet_diagnostic.SST1628.severity = error # Documentation text should begin with a capital letter +dotnet_diagnostic.SST1629.severity = error # Documentation text should end with a period +dotnet_diagnostic.SST1630.severity = error # Documentation text should contain whitespace +dotnet_diagnostic.SST1631.severity = error # Documentation text should be mostly letters +dotnet_diagnostic.SST1632.severity = error # Documentation text should meet a minimum length +dotnet_diagnostic.SST1633.severity = error # Files should begin with the configured header +dotnet_diagnostic.SST1642.severity = error # Constructor summaries should begin with the standard text +dotnet_diagnostic.SST1643.severity = error # Destructor summaries should begin with the standard text +dotnet_diagnostic.SST1644.severity = error # Documentation headers should not contain blank lines +dotnet_diagnostic.SST1648.severity = error # inheritdoc should be used with inheriting elements +dotnet_diagnostic.SST1649.severity = error # The file name should match the first type name +dotnet_diagnostic.SST1651.severity = error # Placeholder documentation elements should be removed +dotnet_diagnostic.SST1653.severity = error # Keep short documentation summaries on a single line +dotnet_diagnostic.SST1654.severity = error # Extension blocks should be documented with a summary +dotnet_diagnostic.SST1655.severity = error # Extension block parameters should be documented +dotnet_diagnostic.SST1656.severity = error # Extension block type parameters should be documented +dotnet_diagnostic.SST1657.severity = error # Extension block documentation should reference a real parameter or type parameter +dotnet_diagnostic.SST1658.severity = error # Documentation text repeats a word +dotnet_diagnostic.SST1659.severity = error # A comment has no text at all +dotnet_diagnostic.SST1660.severity = error # The `` tags are not in parameter order. Code fix reorders them. Info. +dotnet_diagnostic.SST1661.severity = error # A snippet uses ``/`` mismatched to single- vs multi-line content. Code fix swaps the tag. Info. +dotnet_diagnostic.SST1662.severity = none # A thrown exception type has no `` documentation. Code fix adds the skeleton. Opt-in. +dotnet_diagnostic.SST1663.severity = none # A `//` comment before a public member reads like a summary; use `///`. Code fix converts it. Opt-in. +dotnet_diagnostic.SST1664.severity = none # A summary separates paragraphs with blank lines instead of ``. Code fix wraps them. Opt-in. + +# Concurrency and modernization +dotnet_diagnostic.SST1900.severity = error # see docs/rules/SST1900.md +dotnet_diagnostic.SST1901.severity = error # A lock targets a field or property reachable from outside the declaring type +dotnet_diagnostic.SST1902.severity = error # Do not lock on 'this', a Type, or a string +dotnet_diagnostic.SST1903.severity = error # Do not lock on a newly-created object +dotnet_diagnostic.SST1904.severity = error # A lock targets a non-readonly field +dotnet_diagnostic.SST1905.severity = error # An async method or converted delegate returns void +dotnet_diagnostic.SST2000.severity = suggestion # A null check plus throw should use ArgumentNullException.ThrowIfNull +dotnet_diagnostic.SST2001.severity = error # Use ArgumentException.ThrowIfNullOrEmpty +dotnet_diagnostic.SST2002.severity = error # Use ArgumentException.ThrowIfNullOrWhiteSpace +dotnet_diagnostic.SST2003.severity = suggestion # A disposed check should use ObjectDisposedException.ThrowIf +dotnet_diagnostic.SST2004.severity = suggestion # A range check should use an ArgumentOutOfRangeException.ThrowIf... helper +dotnet_diagnostic.SST2005.severity = error # Use the 'is' type pattern instead of comparing an 'as' cast to null +dotnet_diagnostic.SST2006.severity = error # Use the 'is not' pattern instead of negating an 'is' check +dotnet_diagnostic.SST2007.severity = error # Use declaration patterns instead of an is check followed by a cast local +dotnet_diagnostic.SST2008.severity = error # Negated pattern tests should use is-not patterns +dotnet_diagnostic.SST2009.severity = error # A catch that tests then rethrows can use a when filter +dotnet_diagnostic.SST2010.severity = error # A type reads the machine clock directly instead of through a TimeProvider +dotnet_diagnostic.SST2011.severity = error # An instant is recorded from the local clock rather than in UTC +dotnet_diagnostic.SST2012.severity = error # A GUID is constructed with the parameterless constructor instead of Guid.Empty +dotnet_diagnostic.SST2013.severity = error # An if whose entire body is another if should be merged +dotnet_diagnostic.SST2014.severity = error # A goto jumps to a label +dotnet_diagnostic.SST2015.severity = error # A ++ or -- is buried inside a larger expression +dotnet_diagnostic.SST2016.severity = error # A DateTime on a visible signature loses its offset at the boundary +dotnet_diagnostic.SST2017.severity = error # A .Date or .TimeOfDay read proves the value is only a date, or only a time of day +dotnet_diagnostic.SST2018.severity = error # A redundant null check sits beside an is type pattern + +# Modern language and library usage +dotnet_diagnostic.SST1700.severity = error # An extension block declares no members +dotnet_diagnostic.SST1701.severity = error # Two extension blocks in a type share the same receiver type +dotnet_diagnostic.SST1702.severity = error # Extension blocks in a type are separated by other members +dotnet_diagnostic.SST1703.severity = error # A classic this-parameter extension method is used where an extension block could be +dotnet_diagnostic.SST1704.severity = error # A class declaring extension blocks is not named with an Extensions suffix +dotnet_diagnostic.SST1705.severity = error # A class mixes classic extension methods with extension blocks +dotnet_diagnostic.SST1706.severity = error # An extension block targets a broad receiver type such as object or dynamic +dotnet_diagnostic.SST1707.severity = error # Extension blocks should be ordered by receiver type +dotnet_diagnostic.SST1708.severity = error # An extension method never uses its `this` receiver, so it need not be an extension. +dotnet_diagnostic.SST1709.severity = none # A method in a `*Extensions` class whose first parameter lacks `this`. Code fix converts it to an extension block. Opt-in. +dotnet_diagnostic.SST1800.severity = error # Record classes should be sealed +dotnet_diagnostic.SST1801.severity = error # A positional record parameter does not match the configured casing +dotnet_diagnostic.SST1802.severity = error # A record declares a settable rather than init-only instance property +dotnet_diagnostic.SST1803.severity = error # A record struct is not declared readonly +dotnet_diagnostic.SST1804.severity = error # A positional record has an empty `{ }` body where `;` would do. Code fix rewrites it. Info. +dotnet_diagnostic.SST2100.severity = error # An empty collection creation can use [] +dotnet_diagnostic.SST2101.severity = error # An explicit collection creation can use [...] +dotnet_diagnostic.SST2102.severity = error # A span-targeted stackalloc initializer can use a collection expression +dotnet_diagnostic.SST2103.severity = error # A collection-builder factory call can use a collection expression +dotnet_diagnostic.SST2104.severity = error # A short builder-local sequence can return a collection expression +dotnet_diagnostic.SST2105.severity = error # A literal array materialized with LINQ can use the target collection expression directly +dotnet_diagnostic.SST2200.severity = error # A single-use backing field can use the C# 14 field keyword +dotnet_diagnostic.SST2201.severity = error # A return-only switch statement can use a switch expression +dotnet_diagnostic.SST2202.severity = error # An object creation repeats an explicit target type +dotnet_diagnostic.SST2203.severity = error # An array or string index can use from-end indexing +dotnet_diagnostic.SST2204.severity = error # A string slice can use range syntax +dotnet_diagnostic.SST2205.severity = none # An enum switch statement omits named enum values — duplicate of IDE0010 (disabled: too noisy for default/fallthrough) and S131; statement switches deliberately no-op omitted values, and a default to satisfy it is rejected by SST1179/S3532. SST2206 keeps the valuable switch-expression exhaustiveness check. +dotnet_diagnostic.SST2206.severity = error # An enum switch expression omits named enum values +dotnet_diagnostic.SST2207.severity = error # A null guard and return can keep the throw in the returned expression +dotnet_diagnostic.SST2208.severity = error # An out variable can be declared at the call site +dotnet_diagnostic.SST2209.severity = none # A null-forgiving operator has no local effect +dotnet_diagnostic.SST2210.severity = error # A nullable directive repeats the current file-local state +dotnet_diagnostic.SST2211.severity = error # A nullable restore directive has no file-local state to restore +dotnet_diagnostic.SST2212.severity = error # Literal UTF-8 byte data can use a u8 string literal +dotnet_diagnostic.SST2213.severity = error # A typed pattern has an unnecessary discard designation +dotnet_diagnostic.SST2214.severity = error # A tuple temporary only feeds copied element locals +dotnet_diagnostic.SST2215.severity = error # A temporary local swaps two locals +dotnet_diagnostic.SST2216.severity = error # A tuple element name repeats the inferred name +dotnet_diagnostic.SST2217.severity = error # A manual hash-code expression can use System.HashCode.Combine +dotnet_diagnostic.SST2218.severity = error # Lambda parameter types can be omitted when the target already supplies them +dotnet_diagnostic.SST2219.severity = error # A single-expression property accessor can use an expression body +dotnet_diagnostic.SST2220.severity = error # An interpolation hole can carry the value and literal format directly +dotnet_diagnostic.SST2221.severity = error # An ignored expression value is assigned to the discard +dotnet_diagnostic.SST2222.severity = error # A local value is overwritten before it is read +dotnet_diagnostic.SST2223.severity = error # A null fallback assignment can use ??= +dotnet_diagnostic.SST2224.severity = error # An anonymous object can become a tuple literal for local value bundles +dotnet_diagnostic.SST2225.severity = error # A foreach loop hides an explicit element cast +dotnet_diagnostic.SST2226.severity = error # A cast hides an inner explicit conversion +dotnet_diagnostic.SST2227.severity = error # A post-assignment null fallback can be folded into the assigned expression +dotnet_diagnostic.SST2228.severity = error # A delegate local used only as a call target can be a local function +dotnet_diagnostic.SST2229.severity = error # see docs/rules/SST2229.md +dotnet_diagnostic.SST2230.severity = error # see docs/rules/SST2230.md +dotnet_diagnostic.SST2231.severity = error # A broad object pattern can use a direct null pattern +dotnet_diagnostic.SST2232.severity = error # nameof does not need concrete generic type arguments +dotnet_diagnostic.SST2233.severity = none # see docs/rules/SST2233.md +dotnet_diagnostic.SST2234.severity = error # Nullable should use the T? shorthand +dotnet_diagnostic.SST2235.severity = error # Capture-free local functions should be static +dotnet_diagnostic.SST2236.severity = error # Tail-position using blocks can use using declarations +dotnet_diagnostic.SST2237.severity = error # Single block-scoped namespaces can use file-scoped syntax +dotnet_diagnostic.SST2238.severity = error # Nested property patterns can use extended property syntax +dotnet_diagnostic.SST2239.severity = error # Forwarding lambdas can use method groups +dotnet_diagnostic.SST2240.severity = error # Delegate null checks can use conditional invocation +dotnet_diagnostic.SST2241.severity = error # Constructors that only store parameters can use primary-constructor storage +dotnet_diagnostic.SST2242.severity = error # Enum switch statement mappings should name every enum value or include a catch-all +dotnet_diagnostic.SST2243.severity = error # A verbatim string with escapes or line breaks can use a raw string literal +dotnet_diagnostic.SST2244.severity = error # A numeric literal's suffix is lower case +dotnet_diagnostic.SST2245.severity = error # A for loop with only a condition should be a while loop +dotnet_diagnostic.SST2246.severity = error # A chain of conditional expressions testing one value against constants can be a switch expression +dotnet_diagnostic.SST2247.severity = error # Consecutive locals copying one value's members in order can be a deconstruction +dotnet_diagnostic.SST2248.severity = error # Two constant comparisons of the same value can fold into one is-pattern +dotnet_diagnostic.SST2249.severity = error # A literal-format string.Format or literal concatenation can be an interpolated string +dotnet_diagnostic.SST2250.severity = error # A bare local assigned once by the next statement can be an initialized declaration +dotnet_diagnostic.SST2251.severity = error # A method call names type arguments that inference would supply +dotnet_diagnostic.SST2252.severity = error # A switch statement is nested inside another switch statement +dotnet_diagnostic.SST2254.severity = none # A target-typed `new()` is written where an explicit type reads more clearly; the code fix restores `new TypeName(...)`. Opt-in — the counterpart to SST2202's target-typed direction, so a team enables at most one. +dotnet_diagnostic.SST2255.severity = error # A hand-written null-or-empty string test. Code fix uses `string.IsNullOrEmpty`. +dotnet_diagnostic.SST2256.severity = error # An extension method called in static form. Code fix rewrites to instance form. Info. +dotnet_diagnostic.SST2257.severity = error # A lambda block body that is a single `return`. Code fix uses an expression body. Info. +dotnet_diagnostic.SST2258.severity = error # A redundant explicit delegate wrapper (`new EventHandler(M)`). Code fix drops it. Info. +dotnet_diagnostic.SST2259.severity = error # A stray `;` after a type declaration. Code fix removes it. Info. +dotnet_diagnostic.SST2260.severity = error # An `as` cast to a type the operand already has. Code fix removes it. Info. +dotnet_diagnostic.SST2261.severity = error # `(x && !y) +dotnet_diagnostic.SST2262.severity = error # A raw string literal whose content needs no raw syntax. Code fix demotes it. Info. +dotnet_diagnostic.SST2263.severity = error # An infinite loop whose body re-derives its stop condition. Code fix hoists the condition into the header. Info. +dotnet_diagnostic.SST2264.severity = error # A numeric literal cast to an enum. Code fix names the member. +dotnet_diagnostic.SST2265.severity = none # Consecutive fluent calls on one receiver can fold into a chain. Opt-in. +dotnet_diagnostic.SST2266.severity = none # A local read exactly once can be inlined into that use. Opt-in. +dotnet_diagnostic.SST2267.severity = none # Infinite loops written in mixed `while(true)`/`for(;;)` styles. Configurable. Opt-in. +dotnet_diagnostic.SST2268.severity = none # Inconsistent `()` on object creation with an initializer. Configurable. Opt-in. +dotnet_diagnostic.SST2269.severity = none # Inconsistent parentheses around a conditional's condition. Configurable. Opt-in. +dotnet_diagnostic.SST2270.severity = none # Inconsistent explicit-vs-implicit array-creation type. Configurable. Opt-in. +dotnet_diagnostic.SST2271.severity = none # `var`-vs-explicit local type per the configured preference. Configurable. Opt-in. +dotnet_diagnostic.SST2272.severity = none # `[Flags]` member values written as mixed decimals and shifts. Configurable. Opt-in. +dotnet_diagnostic.SST2273.severity = none # A function or loop body wraps its work in a trailing `if` that could be an early-exit guard clause. Code fix inverts it. Configurable threshold. Opt-in. +dotnet_diagnostic.SST2274.severity = error # A value assigned with `as` and then null-checked is an `is` declaration pattern in one step. Code fix rewrites it. +dotnet_diagnostic.SST2275.severity = error # A method whose block body is a single statement can use an expression body `=> expr`. Code fix rewrites it. +dotnet_diagnostic.SST2276.severity = none # A constructor whose block body is a single statement can use an expression body. Code fix rewrites it. Opt-in. +dotnet_diagnostic.SST2277.severity = none # An operator whose block body is a single `return` can use an expression body. Code fix rewrites it. Opt-in. +dotnet_diagnostic.SST2278.severity = none # A conversion operator whose block body is a single `return` can use an expression body. Code fix rewrites it. Opt-in. +dotnet_diagnostic.SST2279.severity = error # A get-only property whose getter is a single `return` can use a whole-member expression body. Code fix rewrites it. +dotnet_diagnostic.SST2280.severity = error # A get-only indexer whose getter is a single `return` can use a whole-member expression body. Code fix rewrites it. +dotnet_diagnostic.SST2281.severity = error # A local function whose block body is a single statement can use an expression body. Code fix rewrites it. +dotnet_diagnostic.SST2282.severity = error # A reference-type `ReferenceEquals` check against `null` reads as an `is null` / `is not null` pattern. Code fix rewrites it. +dotnet_diagnostic.SST2283.severity = error # A null guard that throws right before assigning the guarded value can fold into the assignment as `?? throw`. Code fix rewrites it. +# Design +dotnet_diagnostic.SST2300.severity = error # A class implements IDisposable but builds only half of the disposal pattern +dotnet_diagnostic.SST2301.severity = error # A class implementing IEquatable for itself can still be derived from +dotnet_diagnostic.SST2302.severity = error # A type overloads an operator without the rest of the set it belongs to +dotnet_diagnostic.SST2303.severity = error # A [Flags] enum's members are not distinct bit values +dotnet_diagnostic.SST2304.severity = error # An event's delegate does not have the standard (object sender, TEventArgs e) shape +dotnet_diagnostic.SST2305.severity = error # A mutable collection property declares a caller-visible setter +dotnet_diagnostic.SST2306.severity = error # A collection-returning member hands back null +dotnet_diagnostic.SST2307.severity = error # A generic method's type parameter appears in no parameter, so no caller can infer it +dotnet_diagnostic.SST2308.severity = error # An [Obsolete] attribute carries no message +dotnet_diagnostic.SST2309.severity = error # An externally visible member declares an optional parameter, so callers bake in the default +dotnet_diagnostic.SST2310.severity = error # Deprecated code is still here; remove it once its last caller is gone +dotnet_diagnostic.SST2311.severity = error # A visible const is copied into every assembly that reads it +dotnet_diagnostic.SST2312.severity = error # A type is declared outside any namespace +dotnet_diagnostic.SST2313.severity = error # An enum is stored as a type the project does not allow +dotnet_diagnostic.SST2314.severity = none # An [Obsolete] has a message but no DiagnosticId — unusable here: ObsoleteAttribute.DiagnosticId is .NET 5+, and this source is shared with net462 +dotnet_diagnostic.SST2315.severity = error # A type owns a disposable field but does not implement IDisposable +dotnet_diagnostic.SST2316.severity = error # A type declares Dispose or DisposeAsync without implementing IDisposable +dotnet_diagnostic.SST2317.severity = error # A disposable type owns a raw IntPtr without a SafeHandle or finalizer +dotnet_diagnostic.SST2318.severity = error # Two members have token-identical bodies +dotnet_diagnostic.SST2319.severity = error # An overload's optional default can never be used +dotnet_diagnostic.SST2320.severity = error # An interface inherits two interfaces that declare the same member +dotnet_diagnostic.SST2321.severity = error # Environment.Exit or Environment.FailFast is called from library code +dotnet_diagnostic.SST2322.severity = error # A non-private readonly field holds a mutable collection callers can still change +dotnet_diagnostic.SST2323.severity = error # A stateless abstract class declaring only public abstract members should be an interface +dotnet_diagnostic.SST2324.severity = error # A member is declared more accessible than its containing type +dotnet_diagnostic.SST2325.severity = error # An async method checks an argument after its first await +dotnet_diagnostic.SST2326.severity = none # An interface-typed value is narrowed to a concrete implementation — this is a high-performance core library, not strictly SOLID code, and narrowing to a known runtime type is a normal fast-path technique here: foreach over IEnumerable boxes List's struct enumerator (40 bytes per call, measured) where the indexed loop behind the type test allocates nothing +dotnet_diagnostic.SST2327.severity = error # A type tests its own runtime type against a class instead of dispatching through a virtual member +dotnet_diagnostic.SST2328.severity = error # A raw native pointer handle is exposed instead of a SafeHandle +dotnet_diagnostic.SST2329.severity = error # A `[Flags]` enum declares no zero-valued member. Code fix adds `None = 0`. +dotnet_diagnostic.SST2330.severity = error # A `[Flags]` member is a numeric literal equal to a combination of others (`All = 7`). Code fix writes `A +dotnet_diagnostic.SST2331.severity = none # An enum leaves member values implicit, so their numbers depend on declaration order. Opt-in. +dotnet_diagnostic.SST2332.severity = error # An auto-property's `private set` is only written during construction; make it get-only. +dotnet_diagnostic.SST2333.severity = none # A generic comparison/equality contract is implemented without its non-generic counterpart. Opt-in. +dotnet_diagnostic.SST2334.severity = none # A publicly visible type has no `[DebuggerDisplay]`. Opt-in. +dotnet_diagnostic.SST2335.severity = none # Parts of a partial type disagree on the `static` modifier. Opt-in. + +# Correctness +dotnet_diagnostic.SST2400.severity = error # Two arguments name each other's parameters and have been transposed +dotnet_diagnostic.SST2401.severity = error # A catch targets NullReferenceException +dotnet_diagnostic.SST2402.severity = error # An instance constructor assigns a static field of its own type +dotnet_diagnostic.SST2403.severity = error # 'this' escapes from a constructor before the object is fully built +dotnet_diagnostic.SST2404.severity = error # An iterator's argument guard does not run until the first MoveNext +dotnet_diagnostic.SST2405.severity = error # A [DebuggerDisplay] string names a member the type does not have +dotnet_diagnostic.SST2406.severity = error # A loop's stop condition reads only variables the loop never writes +dotnet_diagnostic.SST2407.severity = error # A declared event is never raised +dotnet_diagnostic.SST2408.severity = error # A StringBuilder is filled and never read +dotnet_diagnostic.SST2409.severity = error # A throw constructs a general exception type (Exception/SystemException/ApplicationException) +dotnet_diagnostic.SST2410.severity = error # A created disposable is never disposed and never leaves the method +dotnet_diagnostic.SST2411.severity = error # A for loop declares and tests a counter it never steps +dotnet_diagnostic.SST2412.severity = error # A for loop update moves the counter away from its stop condition +dotnet_diagnostic.SST2413.severity = error # A for loop condition can never be true on the first pass +dotnet_diagnostic.SST2414.severity = error # Two branches of a conditional share the same implementation +dotnet_diagnostic.SST2415.severity = error # A non-short-circuiting & or | evaluates a right operand that does work +dotnet_diagnostic.SST2416.severity = error # A modulus result on a signed type is compared directly to 1 +dotnet_diagnostic.SST2417.severity = error # A compound assignment operator is transposed (=+, =-, =!) +dotnet_diagnostic.SST2418.severity = error # The result of an immutable value's method is discarded +dotnet_diagnostic.SST2419.severity = error # A set or collection operation is performed against itself +dotnet_diagnostic.SST2420.severity = error # An IndexOf result is tested with > 0, skipping index 0 +dotnet_diagnostic.SST2421.severity = error # A write targets a readonly field of an unconstrained type parameter +dotnet_diagnostic.SST2422.severity = error # A property getter returns a different field than its setter writes +dotnet_diagnostic.SST2423.severity = error # A disposable created in a using statement is returned +dotnet_diagnostic.SST2424.severity = error # An override changes a parameter's default value +dotnet_diagnostic.SST2425.severity = error # An override drops an optional argument on its base call +dotnet_diagnostic.SST2426.severity = error # An override adds or removes params on a parameter +dotnet_diagnostic.SST2427.severity = error # A derived overload widens a parameter and hides the base overload +dotnet_diagnostic.SST2428.severity = error # A static initializer reads a static field declared later +dotnet_diagnostic.SST2429.severity = error # A setter, init, add, or remove accessor never reads value +dotnet_diagnostic.SST2430.severity = error # A serialization callback has the wrong signature +dotnet_diagnostic.SST2431.severity = error # A ToString override can return null +dotnet_diagnostic.SST2432.severity = error # GetType is called on a value that is already a System.Type +dotnet_diagnostic.SST2433.severity = error # A caller-info parameter is not last in the list +dotnet_diagnostic.SST2434.severity = error # An array is assigned through a covariant element type +dotnet_diagnostic.SST2435.severity = error # A non-object base's value-equality Equals is used as a reference-equality fast path +dotnet_diagnostic.SST2436.severity = error # An event is raised with a null sender or null args +dotnet_diagnostic.SST2437.severity = error # A generic type inherits from itself recursively +dotnet_diagnostic.SST2438.severity = error # A catch that discards its exception logs at Error or Critical without it +dotnet_diagnostic.SST2439.severity = error # An exception is passed as a log template argument instead of the exception parameter +dotnet_diagnostic.SST2440.severity = error # Log template arguments are transposed +dotnet_diagnostic.SST2441.severity = error # A log template has an empty or non-identifier placeholder +dotnet_diagnostic.SST2442.severity = error # A log template repeats a named placeholder +dotnet_diagnostic.SST2443.severity = error # An ILogger is injected or created with the wrong category type +dotnet_diagnostic.SST2444.severity = error # A regular expression pattern is invalid +dotnet_diagnostic.SST2445.severity = error # A culture-sensitive custom date or time format is used without an invariant culture +dotnet_diagnostic.SST2446.severity = error # A Stream.ReadAsync result is discarded through ConfigureAwait or a local +dotnet_diagnostic.SST2448.severity = error # A combined or opaque delegate is removed with - or -=, which strips only a contiguous run +dotnet_diagnostic.SST2449.severity = error # A lambda or anonymous-method handler is removed with -=, which never matches it +dotnet_diagnostic.SST2450.severity = error # A Debug.Assert condition performs a side effect that a release build compiles out +dotnet_diagnostic.SST2451.severity = error # Every constructor is private and no member ever creates an instance +dotnet_diagnostic.SST2452.severity = error # A [Pure] method returns void, Task, or ValueTask, so it has no observable result +dotnet_diagnostic.SST2456.severity = error # An override or new field-like event gets its own backing delegate field +dotnet_diagnostic.SST2457.severity = error # An integer Sum wrapped in unchecked still throws on overflow +dotnet_diagnostic.SST2458.severity = error # A bitwise operator is applied to an enum not declared [Flags] +dotnet_diagnostic.SST2459.severity = error # [Optional] on a ref or out parameter advertises an optionality no caller can use +dotnet_diagnostic.SST2460.severity = error # [DefaultValue] on a method or record parameter is inert +dotnet_diagnostic.SST2462.severity = error # A new member is less accessible than the inherited member it hides +dotnet_diagnostic.SST2463.severity = error # A field differs from an inherited accessible field only by case +dotnet_diagnostic.SST2464.severity = error # A mutable class declares a value-equality operator ==, so it is lost as a hash key +dotnet_diagnostic.SST2465.severity = error # A for loop body reassigns the counter or the local its condition tests +dotnet_diagnostic.SST2467.severity = error # A params overload is shadowed by a same-arity overload with a more specific last parameter +dotnet_diagnostic.SST2468.severity = error # A classic partial method is declared but never implemented, so its calls are removed +dotnet_diagnostic.SST2470.severity = error # Two string literals concatenate with no space, fusing a SQL keyword into the next token +dotnet_diagnostic.SST2472.severity = error # A type is exported for a contract it neither implements nor inherits +dotnet_diagnostic.SST2473.severity = error # A shared export part is constructed with new, bypassing the container +dotnet_diagnostic.SST2474.severity = error # A part-creation-policy attribute is applied to a type with no [Export] +dotnet_diagnostic.SST2475.severity = error # An entity's primary key is typed DateTime or DateTimeOffset +dotnet_diagnostic.SST2479.severity = error # A loop variable captured by a callback stored beyond the iteration reads its final value +dotnet_diagnostic.SST2481.severity = error # A GetHashCode override folds the base identity hash into a value hash +dotnet_diagnostic.SST2484.severity = error # A handle read through DangerousGetHandle is not reference-counted +dotnet_diagnostic.SST2485.severity = error # A NotImplementedException is left in shipped code +dotnet_diagnostic.SST2486.severity = error # An assembly is loaded by path or partial name instead of Assembly.Load +dotnet_diagnostic.SST2487.severity = error # A [ConstructorArgument] does not name a constructor parameter +dotnet_diagnostic.SST2488.severity = error # An exception is logged and rethrown, duplicating the record +dotnet_diagnostic.SST2489.severity = error # A relational comparison is decided by the operand's type rather than its value +dotnet_diagnostic.SST2490.severity = error # Adjacent try statements with identical handling should be merged +dotnet_diagnostic.SST2491.severity = error # A non-`async` method returns an awaitable from inside `using`/`try-finally`/`lock`, so the resource is torn down before the task completes. Code fix makes it `async`. +dotnet_diagnostic.SST2492.severity = error # A null-guard throws on a parameter the signature declares may be null. +dotnet_diagnostic.SST2493.severity = error # `== null`/`!= null` on an unconstrained generic `T`. Code fix uses `is null`/`is not null`. +dotnet_diagnostic.SST2494.severity = error # A `??` whose left operand is a constant null, so the right is always taken. Code fix folds it. +dotnet_diagnostic.SST2495.severity = error # A `[Flags]` combination includes an operand whose bits another already covers. Code fix removes it. +dotnet_diagnostic.SST2496.severity = error # An explicit `Dispose`/`Close` on a resource an enclosing `using` already disposes. Code fix removes it. Info. + +# Testing +dotnet_diagnostic.SST2500.severity = error # A test method contains no assertion and no expected-exception check +dotnet_diagnostic.SST2501.severity = error # An equality or identity assertion compares an expression with itself +dotnet_diagnostic.SST2502.severity = error # An equality assertion passes the constant as actual and the computed value as expected +dotnet_diagnostic.SST2503.severity = error # An equality assertion compares a value against a boolean literal +dotnet_diagnostic.SST2504.severity = error # A test fixture declares and inherits no test method +dotnet_diagnostic.SST2505.severity = error # A test method declares parameters but no data source +dotnet_diagnostic.SST2506.severity = error # A test method calls Thread.Sleep +dotnet_diagnostic.SST2507.severity = error # A test method declares its expected failure with an expected-exception attribute +dotnet_diagnostic.SST2508.severity = error # A fluent assertion is started but never completed +dotnet_diagnostic.SST2509.severity = error # A test method has a shape the runner cannot execute + +# Logging +dotnet_diagnostic.SST2600.severity = error # Application output is written through legacy Trace instead of a structured logger +dotnet_diagnostic.SST2601.severity = error # A logger field or property does not follow the logger naming convention + +# Frameworks +dotnet_diagnostic.SST2700.severity = error # An MVC route template contains a backslash; route segments are separated by `/`, so the route is unreachable. Code fix replaces `\` with `/`. +dotnet_diagnostic.SST2701.severity = error # A `[JSInvokable]` method is not public, so JavaScript interop cannot call it. Code fix makes it public. +dotnet_diagnostic.SST2702.severity = error # A `[SupplyParameterFromQuery]` property has a type the framework cannot bind from the query string, which throws at runtime. +dotnet_diagnostic.SST2703.severity = error # A routable component's route constraint (`{id:int}`) disagrees with the matching `[Parameter]` CLR type, so the route silently fails to match. +dotnet_diagnostic.SST2704.severity = error # A public action on an `[ApiController]` declares no HTTP-verb attribute, so it answers every verb and can make routing ambiguous. +dotnet_diagnostic.SST2705.severity = none # A bound model member is a non-nullable value type with no required marker, so a request that omits it binds the default with no error. Opt-in. +dotnet_diagnostic.SST2706.severity = error # A Windows Forms entry point carries neither `[STAThread]` nor `[MTAThread]`; without STA, clipboard, drag-and-drop, and common dialogs misbehave. Code fix adds `[STAThread]`. +dotnet_diagnostic.SST2707.severity = none # A fire-and-forget `Task.Run` in a controller captures the request's `HttpContext`, which is disposed when the request ends, so the background work throws `ObjectDisposedException`. Opt-in. +dotnet_diagnostic.SST2708.severity = error # A component subscribes to an event in a lifecycle method but never unsubscribes, so the event source keeps the component alive — a per-session leak on a Server circuit. +dotnet_diagnostic.SST2709.severity = error # `StateHasChanged` is called while the component is being disposed, which the renderer no longer supports and throws. +dotnet_diagnostic.SST2710.severity = error # `StateHasChanged` is called directly from a timer callback, off the renderer's dispatcher; marshal it with `InvokeAsync(StateHasChanged)`. +dotnet_diagnostic.SST2711.severity = error # A synchronous component lifecycle method is overridden as `async void`, which the framework never awaits; override the `…Async` twin returning `Task`. Code fix rewrites the signature. +dotnet_diagnostic.SST2712.severity = error # An `[Inject]`/`[CascadingParameter]` property has no setter, so the framework's reflection-based binding leaves it null. Code fix adds a setter. +dotnet_diagnostic.SST2713.severity = error # A `DotNetObjectReference.Create(this)` is passed inline and never stored, so nothing can dispose it and it leaks on the JavaScript side. + +################### +# PerformanceSharp Analyzers (PSH) +################### +performancesharp.avoid_linq_on_hot_path = true +# performancesharp.empty_string_style = pattern # PSH1204 (pattern | length | is_null_or_empty; the last two are only offered where the string is provably not null) +# performancesharp.excluded_properties = Items, Keys # PSH1017 (comma-separated; properties allowed to copy on read) +# performancesharp.include_public = false # PSH1411 (set true in an app to seal public types too; a break in a library) +dotnet_diagnostic.PSH1000.severity = error # Anonymous functions without captures should be static +dotnet_diagnostic.PSH1001.severity = error # Avoid allocating zero-length arrays (fix prefers [] on C# 12+, else Array.Empty()) +dotnet_diagnostic.PSH1002.severity = error # Empty finalizers should be removed +dotnet_diagnostic.PSH1003.severity = error # 'in' parameters should use readonly structs +dotnet_diagnostic.PSH1004.severity = error # Constant arrays passed as arguments should be hoisted +dotnet_diagnostic.PSH1005.severity = error # Structs should define equality members to avoid boxing comparisons +dotnet_diagnostic.PSH1006.severity = error # ConcurrentDictionary factories should use the lambda argument +dotnet_diagnostic.PSH1007.severity = error # Pass large readonly structs by 'in' reference (size threshold + exclusions configurable) +dotnet_diagnostic.PSH1008.severity = error # GC.SuppressFinalize does nothing for sealed finalizer-free types +dotnet_diagnostic.PSH1009.severity = error # Bound variable-length stackalloc with a constant guard +dotnet_diagnostic.PSH1010.severity = error # Clear reference-typed arrays when returning them to the pool +dotnet_diagnostic.PSH1011.severity = error # Pass state to callbacks through the state-taking overload +dotnet_diagnostic.PSH1012.severity = error # Compare type parameter values with EqualityComparer.Default +dotnet_diagnostic.PSH1013.severity = error # Expose constant UTF-8 data as a ReadOnlySpan property +dotnet_diagnostic.PSH1014.severity = error # Declare immutable structs as readonly +dotnet_diagnostic.PSH1015.severity = error # Avoid casting value types through object +dotnet_diagnostic.PSH1016.severity = error # Test enum flags with bitwise operators instead of Enum.HasFlag +dotnet_diagnostic.PSH1017.severity = error # A property allocates a copy of a collection on every read (excludable via performancesharp.PSH1017.excluded_properties) +dotnet_diagnostic.PSH1018.severity = error # A hand-written array is passed to a params parameter +dotnet_diagnostic.PSH1019.severity = error # The range indexer on an array allocates a copy; slice with AsSpan/AsMemory +dotnet_diagnostic.PSH1020.severity = error # Prefer a jagged array over a multidimensional one +dotnet_diagnostic.PSH1021.severity = error # An explicit GC.Collect or GC.WaitForPendingFinalizers forces collection the runtime tunes itself +dotnet_diagnostic.PSH1022.severity = error # A parameterless `new EventArgs()` allocates where the shared `EventArgs.Empty` singleton would serve. Code fix uses the singleton. +dotnet_diagnostic.PSH1100.severity = error # Hot-path code should avoid System.Linq.Enumerable calls +dotnet_diagnostic.PSH1101.severity = none # LINQ terminal predicate simplification is reserved for test code; production code should avoid LINQ on hot paths +dotnet_diagnostic.PSH1102.severity = none # LINQ type-filter simplification is reserved for test code; production code should avoid LINQ on hot paths +dotnet_diagnostic.PSH1103.severity = error # Prefer the collection's own count over enumerating +dotnet_diagnostic.PSH1104.severity = error # Use TryGetValue instead of ContainsKey followed by an indexer read +dotnet_diagnostic.PSH1105.severity = error # Avoid double lookups on dictionaries and sets +dotnet_diagnostic.PSH1106.severity = error # Index collections directly instead of using LINQ element access +dotnet_diagnostic.PSH1107.severity = error # Filter sequences before sorting them +dotnet_diagnostic.PSH1108.severity = error # Chain secondary sorts with ThenBy +dotnet_diagnostic.PSH1109.severity = error # Merge consecutive Where calls +dotnet_diagnostic.PSH1110.severity = error # Use the collection's own predicate methods over LINQ +dotnet_diagnostic.PSH1111.severity = error # Use Contains for membership tests +dotnet_diagnostic.PSH1112.severity = error # Seed the collection through its constructor (fix honors performancesharp.prefer_collection_expressions) +dotnet_diagnostic.PSH1113.severity = error # Sort naturally instead of ordering by the element itself +dotnet_diagnostic.PSH1114.severity = none # Freeze static lookup collections that are never mutated. Opt-in. +dotnet_diagnostic.PSH1115.severity = error # Insert-if-absent should probe the dictionary once +dotnet_diagnostic.PSH1116.severity = error # Probe string-keyed collections with a span through GetAlternateLookup +dotnet_diagnostic.PSH1117.severity = error # Ask the collection whether it is empty +dotnet_diagnostic.PSH1118.severity = error # Take the extreme element with Min/Max/MinBy/MaxBy instead of sorting +dotnet_diagnostic.PSH1119.severity = error # Check for elements with Any instead of counting them all +dotnet_diagnostic.PSH1120.severity = error # Do not materialize a sequence with ToList/ToArray just to enumerate it +dotnet_diagnostic.PSH1122.severity = error # Read a sorted set's extreme through its Min/Max property, not the LINQ extension +dotnet_diagnostic.PSH1124.severity = error # Read a linked list's end through its First/Last property, not the LINQ extension +dotnet_diagnostic.PSH1125.severity = error # Do not enumerate the same lazy sequence twice +dotnet_diagnostic.PSH1126.severity = error # Ask whether an async sequence has elements instead of counting them +dotnet_diagnostic.PSH1127.severity = error # Clear an array instead of filling it with its default +dotnet_diagnostic.PSH1200.severity = error # Compare strings without allocating case-converted copies +dotnet_diagnostic.PSH1201.severity = error # Use the char overload for single-character strings +dotnet_diagnostic.PSH1202.severity = error # Append characters as char, not single-character strings +dotnet_diagnostic.PSH1203.severity = error # Let StringBuilder do the formatting work +dotnet_diagnostic.PSH1204.severity = error # Test for empty strings by length +dotnet_diagnostic.PSH1205.severity = error # Remove interpolation that does no work +dotnet_diagnostic.PSH1206.severity = error # Do not build strings by concatenation in loops +dotnet_diagnostic.PSH1207.severity = error # Specify StringComparison for culture-sensitive string operations +dotnet_diagnostic.PSH1208.severity = error # Encode constant strings with u8 literals +dotnet_diagnostic.PSH1209.severity = error # Build transformed strings with string.Create +dotnet_diagnostic.PSH1210.severity = error # Compare UTF-8 bytes without decoding them +dotnet_diagnostic.PSH1211.severity = error # Pass values directly instead of ToString results +dotnet_diagnostic.PSH1212.severity = error # Slice with AsSpan when the call accepts a span +dotnet_diagnostic.PSH1213.severity = error # Probe repeated character sets through SearchValues +dotnet_diagnostic.PSH1214.severity = error # Append the parts of a concatenation separately, not the concatenated whole +dotnet_diagnostic.PSH1215.severity = error # Use string.Concat instead of string.Join with an empty separator +dotnet_diagnostic.PSH1216.severity = error # Use string.Equals instead of comparing string.Compare to zero +dotnet_diagnostic.PSH1217.severity = error # A sequence is copied to an array just to be read straight back +dotnet_diagnostic.PSH1218.severity = error # A substring is allocated only to search it; slice with AsSpan instead +dotnet_diagnostic.PSH1219.severity = error # Ask whether a string is blank without trimming it +dotnet_diagnostic.PSH1220.severity = error # A length argument spells out the run that reaches the end anyway +dotnet_diagnostic.PSH1221.severity = error # An IndexOf compared to 0 scans the whole string to answer a prefix test +dotnet_diagnostic.PSH1222.severity = error # Concatenate slices without materializing them +dotnet_diagnostic.PSH1223.severity = error # A reused composite format string is re-parsed on every call +dotnet_diagnostic.PSH1224.severity = error # Convert bytes to hex in one call, not by building the string twice +dotnet_diagnostic.PSH1225.severity = error # Decode bytes to a string in one call, without a throwaway char[] +dotnet_diagnostic.PSH1226.severity = error # A string's `ToCharArray()` result is only iterated, allocating a throwaway `char[]`; iterate the string directly. Code fix drops the copy. +dotnet_diagnostic.PSH1227.severity = error # A cheaper equivalent exists — `string.CompareOrdinal` over `Compare(…, Ordinal)`, `Debug.Fail` over `Debug.Assert(false, …)`. Info. Code fix rewrites the call. +dotnet_diagnostic.PSH1300.severity = error # Use System.Threading.Lock for a dedicated lock object +dotnet_diagnostic.PSH1301.severity = error # Do not wrap a single task in WhenAll or WaitAll +dotnet_diagnostic.PSH1302.severity = error # TaskCompletionSource should run continuations asynchronously +dotnet_diagnostic.PSH1303.severity = error # Do not block an async method with Thread.Sleep +dotnet_diagnostic.PSH1304.severity = error # Use PeriodicTimer instead of pacing a loop with Task.Delay +dotnet_diagnostic.PSH1305.severity = error # Enumerate a ConcurrentDictionary directly, not its Keys/Values snapshots +dotnet_diagnostic.PSH1306.severity = none # Guard one-time execution with an interlocked latch. Opt-in. +dotnet_diagnostic.PSH1307.severity = error # Access interlocked fields with Volatile +dotnet_diagnostic.PSH1308.severity = error # Return the completed task instead of Task.FromResult +dotnet_diagnostic.PSH1309.severity = none # Register cancellation callbacks without flowing the execution context. Opt-in. +dotnet_diagnostic.PSH1310.severity = error # Dispose IAsyncDisposable resources with await using in async code +dotnet_diagnostic.PSH1311.severity = error # Remove a pass-through async state machine and return the task directly +dotnet_diagnostic.PSH1312.severity = error # Return a completed task instead of null +dotnet_diagnostic.PSH1313.severity = error # A synchronous call where an async overload fits +dotnet_diagnostic.PSH1314.severity = error # Read and write streams through the memory-based overloads +dotnet_diagnostic.PSH1315.severity = error # A blocking wait on an awaitable that may not be done +dotnet_diagnostic.PSH1316.severity = error # A ValueTask is awaited in a loop or awaited after being copied +dotnet_diagnostic.PSH1400.severity = error # Use the static HashData method for one-shot hashing +dotnet_diagnostic.PSH1401.severity = error # Attribute types should be sealed +dotnet_diagnostic.PSH1402.severity = error # Use const for compile-time constants +dotnet_diagnostic.PSH1403.severity = error # Do not initialize fields to their default value +dotnet_diagnostic.PSH1404.severity = error # Get the assembly from typeof instead of a stack walk +dotnet_diagnostic.PSH1405.severity = error # Use the direct Environment APIs +dotnet_diagnostic.PSH1406.severity = error # Ask Regex for the answer directly +dotnet_diagnostic.PSH1407.severity = error # Query the dictionary, not its Keys view +dotnet_diagnostic.PSH1408.severity = error # Measure elapsed time with Stopwatch timestamps +dotnet_diagnostic.PSH1409.severity = error # Use the built-in throw helpers for argument guards +dotnet_diagnostic.PSH1410.severity = none # Mark trivial forwarders for aggressive inlining. Opt-in. +dotnet_diagnostic.PSH1411.severity = error # Seal non-public types nothing derives from so the JIT can devirtualize +dotnet_diagnostic.PSH1412.severity = error # Use Random.Shared instead of allocating a Random +dotnet_diagnostic.PSH1413.severity = error # Read the Unix epoch from the framework, not a hand-built DateTime +dotnet_diagnostic.PSH1414.severity = error # Mark members that do not touch instance state as static +dotnet_diagnostic.PSH1415.severity = error # Hold the concrete type when the concrete type is what you have +dotnet_diagnostic.PSH1416.severity = error # Cache the serializer options instead of building them per call +dotnet_diagnostic.PSH1417.severity = error # Do not compute an expensive argument for an assertion +dotnet_diagnostic.PSH1418.severity = error # An HttpClient is constructed on every call +dotnet_diagnostic.PSH1419.severity = error # A time-zone is resolved with a platform-specific id instead of the cross-platform API +dotnet_diagnostic.PSH1420.severity = error # A shareable client held in an instance field of an Azure Functions worker class is rebuilt on every invocation, leaking sockets and connections; share a static/singleton client or inject `IHttpClientFactory`. + +# ASP.NET Core - inert in this repo (no route handlers or middleware), enabled so the set stays complete +dotnet_diagnostic.PSH1500.severity = error # A minimal API handler returns Results instead of TypedResults, boxing the result +dotnet_diagnostic.PSH1501.severity = error # Middleware uses the legacy nested-delegate Use overload, allocating a per-request closure +dotnet_diagnostic.PSH1502.severity = error # A route handler returns a deferred sequence the serializer enumerates on the request thread +dotnet_diagnostic.PSH1503.severity = error # Response caching is used where server-side output caching applies +dotnet_diagnostic.PSH1505.severity = error # Exceptions are handled in an MVC exception filter instead of an IExceptionHandler +dotnet_diagnostic.PSH1506.severity = error # The HTTP request or response body is read or written synchronously (`ReadToEnd`, `Body.Read`, `Body.Write`), which blocks a thread on Kestrel and buffers the whole payload; use the async overload. Code fix awaits it when the method is already async. + +# Blazor +dotnet_diagnostic.PSH1600.severity = error # A delegate captured per iteration inside a component render loop reallocates on every render (measured ~128 B per row per render) and churns the diff; hoist it to a cached delegate or a precomputed per-item model. +dotnet_diagnostic.PSH1601.severity = error # A JavaScript-interop call is issued once per loop iteration; on Interactive Server each is a separate SignalR round-trip. Batch into a single call over the collection. +dotnet_diagnostic.PSH1602.severity = error # `StateHasChanged` is called unconditionally in `OnAfterRender`/`OnAfterRenderAsync`, scheduling another render every time — a runaway loop. Guard it with `firstRender` or a state flag. +dotnet_diagnostic.PSH1603.severity = error # A non-delegate allocation is used as a component-parameter value inside a render loop, allocating per item and forcing the child to re-render each pass. Sibling of PSH1600. + +################### +# SecuritySharp Analyzers (SES) +################### +# Every rule is raised to error, including the three that ship at suggestion (SES1403, SES1506, +# SES1605). Security rules only ever suggest an API they can resolve in the compilation and report +# local shapes only - there is no interprocedural taint tracking, so the taint-flow CA rules stay on. +# securitysharp.SES1003.iterations = 100000 # minimum accepted PBKDF2 iteration count +# securitysharp.SES1403.maxdepth = 64 # highest accepted System.Text.Json MaxDepth + +# Cryptography +dotnet_diagnostic.SES1001.severity = error # AEAD encryption must not use a constant or reused nonce +dotnet_diagnostic.SES1002.severity = error # Password-based key derivation must not use a constant or predictable salt +dotnet_diagnostic.SES1003.severity = error # Password-based key derivation must use a sufficient iteration count +dotnet_diagnostic.SES1004.severity = error # A secret must not be produced from Guid.NewGuid() +dotnet_diagnostic.SES1005.severity = error # Compare secret values in constant time +dotnet_diagnostic.SES1006.severity = error # A Data Protection key ring is persisted without a ProtectKeysWith call, so keys sit unencrypted at rest +dotnet_diagnostic.SES1007.severity = error # A cryptographic primitive is implemented by hand instead of using a vetted platform algorithm +dotnet_diagnostic.SES1008.severity = error # An XML signature is verified with the no-key CheckSignature overload, trusting the document's own KeyInfo +dotnet_diagnostic.SES1009.severity = error # A password is stored without a slow, salted key-derivation function + +# Transport +dotnet_diagnostic.SES1102.severity = error # Do not accept any server certificate +dotnet_diagnostic.SES1104.severity = error # Certificate-chain validation must not be deliberately weakened +dotnet_diagnostic.SES1105.severity = error # Bearer and OpenID Connect metadata must not be retrieved over plain HTTP outside development +dotnet_diagnostic.SES1106.severity = error # Do not send HttpClient requests to a cleartext http URL +dotnet_diagnostic.SES1107.severity = error # A SQL connection string weakens transport security via TrustServerCertificate or a disabled Encrypt +dotnet_diagnostic.SES1108.severity = error # A custom server-certificate validation callback unconditionally returns true, so any certificate is trusted + +# Secrets +dotnet_diagnostic.SES1201.severity = error # Do not hard-code secrets in source +dotnet_diagnostic.SES1202.severity = error # Do not hard-code a credential value +dotnet_diagnostic.SES1203.severity = error # A connection string names a user but supplies an empty or missing password + +# Injection +dotnet_diagnostic.SES1301.severity = error # Do not build a process command line from non-constant string parts +dotnet_diagnostic.SES1302.severity = error # A shell-executed process must not use a non-constant FileName +dotnet_diagnostic.SES1303.severity = error # Regular-expression pattern must not be built from non-constant data +dotnet_diagnostic.SES1304.severity = error # An archive entry name must not build a write path without a containment check +dotnet_diagnostic.SES1305.severity = error # Do not build a storage path from an uploaded file name +dotnet_diagnostic.SES1306.severity = error # Do not compile or execute non-constant C# via the scripting API +dotnet_diagnostic.SES1307.severity = error # Path.GetTempFileName creates a predictable, world-readable temporary file +dotnet_diagnostic.SES1308.severity = error # A file or directory is created group- or world-writable +dotnet_diagnostic.SES1309.severity = error # An XSLT stylesheet is loaded with embedded script enabled +dotnet_diagnostic.SES1310.severity = error # A directory bind is performed without authenticating + +# Serialization +dotnet_diagnostic.SES1401.severity = error # A type resolved from non-constant data must not be instantiated or deserialized +dotnet_diagnostic.SES1402.severity = error # Do not load an assembly from raw bytes or a non-constant location +dotnet_diagnostic.SES1403.severity = error # JSON deserialization depth limit must stay within a safe ceiling +dotnet_diagnostic.SES1404.severity = error # A type is instantiated by name from a non-constant Activator typeName +dotnet_diagnostic.SES1405.severity = error # MessagePack typeless deserialization reconstructs whatever type the payload names +dotnet_diagnostic.SES1406.severity = warning # Reflection must not reach non-public members via BindingFlags.NonPublic (opt-in; replaces S3011) + +# Web hardening +dotnet_diagnostic.SES1501.severity = error # A CORS policy must not allow credentials together with any origin +dotnet_diagnostic.SES1502.severity = error # A CORS origin predicate must not unconditionally allow every origin +dotnet_diagnostic.SES1503.severity = error # JWT signature verification must not be disabled on TokenValidationParameters +dotnet_diagnostic.SES1504.severity = error # A cookie with SameSite=None must be marked Secure +dotnet_diagnostic.SES1505.severity = error # The request body size limit must not be removed +dotnet_diagnostic.SES1506.severity = error # The developer exception page must be guarded by a development-environment check +dotnet_diagnostic.SES1507.severity = error # AllowAnonymous and Authorize on the same declaration conflict +dotnet_diagnostic.SES1508.severity = error # A validation method must not fail open by returning success from a catch +dotnet_diagnostic.SES1509.severity = error # A backtracking-prone constant regex runs without a match timeout or NonBacktracking +dotnet_diagnostic.SES1510.severity = error # A controller redirects to a non-constant URL, allowing an open redirect +dotnet_diagnostic.SES1511.severity = error # The forwarded-headers trust boundary is cleared, letting proxies spoof the client IP +dotnet_diagnostic.SES1512.severity = error # Sensitive framework diagnostics are enabled without a development-environment guard +dotnet_diagnostic.SES1513.severity = error # An AuthorizeAsync result is discarded, so the guarded operation runs regardless +dotnet_diagnostic.SES1514.severity = error # OpenID Connect protections (PKCE, state, nonce) are disabled +dotnet_diagnostic.SES1515.severity = error # A Content-Security-Policy value disables its own protection + +# AI trust boundaries +dotnet_diagnostic.SES1601.severity = error # An LLM system prompt must be a constant, trusted template +dotnet_diagnostic.SES1602.severity = error # Do not route AI model output into a process, file, or raw SQL sink +dotnet_diagnostic.SES1603.severity = error # An AI tool declared read-only or non-destructive must not call a state-changing API +dotnet_diagnostic.SES1604.severity = error # Prompt-template input encoding must not be disabled +dotnet_diagnostic.SES1605.severity = error # AI instrumentation must not enable sensitive-data capture +dotnet_diagnostic.SES1606.severity = error # Do not fetch model weights over cleartext HTTP + +# Web UI trust boundaries +dotnet_diagnostic.SES1701.severity = error # Raw HTML is rendered from a non-constant value (`MarkupString`/`AddMarkupContent`), bypassing automatic encoding — an XSS risk. Sanitizer allow-list via `securitysharp.SES1701.sanitizers`. +dotnet_diagnostic.SES1702.severity = error # A JavaScript-interop call targets a script-evaluation primitive (`eval`, `Function`, `document.write`), turning interop into a script-injection channel. +dotnet_diagnostic.SES1703.severity = error # `[Authorize]` on a non-routable component enforces nothing — authorization runs as a routing concern. Exempt types via `securitysharp.SES1703.exempt_types`. +dotnet_diagnostic.SES1704.severity = error # `IHttpContextAccessor` or a cascading `HttpContext` is used in an interactively-rendered component, where it is null or frozen at circuit start. +dotnet_diagnostic.SES1705.severity = error # `NavigationManager.NavigateTo` is called with a target that is not a verified relative URL — an open-redirect risk. Validator allow-list via `securitysharp.SES1705.validators`. +dotnet_diagnostic.SES1706.severity = error # An uploaded file is read with an unbounded or client-chosen size limit, letting an attacker fill server memory. Threshold via `securitysharp.SES1706.max_bytes`. +dotnet_diagnostic.SES1707.severity = error # A secret-shaped literal appears in code reachable as WebAssembly, which downloads to the browser in full — guaranteed disclosure. +dotnet_diagnostic.SES1708.severity = error # `CircuitOptions.DetailedErrors` is enabled, shipping server exception detail to every connected client. +dotnet_diagnostic.SES1709.severity = error # `SerializeAllClaims` serializes every claim into client-readable WebAssembly authentication state, exposing internal ids, tokens, and PII. +dotnet_diagnostic.SES1710.severity = error # Antiforgery validation is disabled on a form (`[RequireAntiforgeryToken(required: false)]`), removing CSRF protection. + + +################### +# Microsoft.CodeAnalysis.PublicApiAnalyzers (RS) +################### +# Public API surface tracking +dotnet_diagnostic.RS0016.severity = error # public symbol missing from the PublicAPI baseline +dotnet_diagnostic.RS0017.severity = error # PublicAPI baseline entry no longer in source + +################### +# Microsoft.NET.ILLink.Analyzers (IL) +################### +# Trimming +# See: https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-warnings/ +dotnet_diagnostic.IL2001.severity = error # Type in UnreferencedCode attribute doesn't have matching RequiresUnreferencedCode +dotnet_diagnostic.IL2002.severity = error # Method with RequiresUnreferencedCode called from code without that attribute +dotnet_diagnostic.IL2003.severity = error # RequiresUnreferencedCode attribute is only supported on methods +dotnet_diagnostic.IL2004.severity = error # Incorrect RequiresUnreferencedCode signature +dotnet_diagnostic.IL2005.severity = error # Could not resolve dependency assembly +dotnet_diagnostic.IL2007.severity = error # Could not process embedded resource +dotnet_diagnostic.IL2008.severity = error # Could not find type in assembly +dotnet_diagnostic.IL2009.severity = error # Could not find method in type +dotnet_diagnostic.IL2010.severity = error # Invalid value for PreserveDependencyAttribute +dotnet_diagnostic.IL2011.severity = error # Unknown body modification +dotnet_diagnostic.IL2012.severity = error # Could not find field in type +dotnet_diagnostic.IL2013.severity = error # Substitution file contains invalid XML +dotnet_diagnostic.IL2014.severity = error # Missing substitution file +dotnet_diagnostic.IL2015.severity = error # Invalid XML encountered in substitution file +dotnet_diagnostic.IL2016.severity = error # Could not find type from substitution XML +dotnet_diagnostic.IL2017.severity = error # Could not find method in type specified in substitution XML +dotnet_diagnostic.IL2018.severity = error # Could not find field in type specified in substitution XML +dotnet_diagnostic.IL2019.severity = error # Could not find interface implementation in type +dotnet_diagnostic.IL2022.severity = error # Type in DynamicallyAccessedMembers attribute doesn't have matching DynamicallyAccessedMembers annotation +dotnet_diagnostic.IL2023.severity = error # Method returning DynamicallyAccessedMembers annotated type requires the same annotation +dotnet_diagnostic.IL2024.severity = error # Multiple DynamicallyAccessedMembers annotations on a member are not supported +dotnet_diagnostic.IL2025.severity = error # Duplicate preserve attribute +dotnet_diagnostic.IL2026.severity = error # Using member annotated with RequiresUnreferencedCode +dotnet_diagnostic.IL2027.severity = error # RequiresUnreferencedCodeAttribute is only supported on methods and constructors +dotnet_diagnostic.IL2028.severity = error # Invalid RequiresUnreferencedCode attribute usage +dotnet_diagnostic.IL2029.severity = error # RequiresUnreferencedCode attribute on type is not supported +dotnet_diagnostic.IL2030.severity = error # Dynamic invocation of method requiring unreferenced code is not safe +dotnet_diagnostic.IL2031.severity = error # Could not resolve dependency assembly from embedded resource +dotnet_diagnostic.IL2032.severity = error # Error reading debug symbols +dotnet_diagnostic.IL2033.severity = error # Trying to modify a sealed type +dotnet_diagnostic.IL2034.severity = error # Value passed to the implicit 'this' parameter does not satisfy 'DynamicallyAccessedMembersAttribute' requirements +dotnet_diagnostic.IL2035.severity = error # Unrecognized value passed to the parameter of method with 'DynamicallyAccessedMembersAttribute' requirements +dotnet_diagnostic.IL2036.severity = error # Interface implementation has different DynamicallyAccessedMembers annotations than interface +dotnet_diagnostic.IL2037.severity = error # BaseType annotation doesn't match +dotnet_diagnostic.IL2038.severity = error # Derived type doesn't have matching DynamicallyAccessedMembers annotation +dotnet_diagnostic.IL2039.severity = error # Implementation method doesn't have matching DynamicallyAccessedMembers annotation +dotnet_diagnostic.IL2040.severity = error # Interface member doesn't have matching DynamicallyAccessedMembers annotation +dotnet_diagnostic.IL2041.severity = error # GetType call on DynamicallyAccessedMembers annotated generic parameter +dotnet_diagnostic.IL2042.severity = error # The DynamicallyAccessedMembersAttribute value used in a custom attribute is not compatible +dotnet_diagnostic.IL2043.severity = error # DynamicallyAccessedMembersAttribute on property conflicts with base property +dotnet_diagnostic.IL2044.severity = error # DynamicallyAccessedMembersAttribute on event conflicts with base event +dotnet_diagnostic.IL2045.severity = error # Field type doesn't satisfy 'DynamicallyAccessedMembersAttribute' requirements +dotnet_diagnostic.IL2046.severity = error # Trimmer couldn't find PreserveBaseOverridesAttribute on a method +dotnet_diagnostic.IL2048.severity = error # Internal attribute couldn't be removed +dotnet_diagnostic.IL2049.severity = error # Could not process data format message +dotnet_diagnostic.IL2050.severity = error # Correctness of COM interop cannot be guaranteed after trimming +dotnet_diagnostic.IL2051.severity = error # COM related type is trimmed +dotnet_diagnostic.IL2052.severity = error # Resolving member reference for P/Invoke into type that is trimmed +dotnet_diagnostic.IL2053.severity = error # Target method is trimmed +dotnet_diagnostic.IL2054.severity = error # Generic constraint type is annotated with DynamicallyAccessedMembersAttribute which requires unreferenced code +dotnet_diagnostic.IL2055.severity = error # Type implements COM visible type but has no GUID +dotnet_diagnostic.IL2056.severity = error # Generic parameter with DynamicallyAccessedMembers annotation is not publicly visible +dotnet_diagnostic.IL2057.severity = error # Unrecognized value passed to the parameter of method with DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2058.severity = error # Parameter types of method doesn't have matching DynamicallyAccessedMembers annotation +dotnet_diagnostic.IL2059.severity = error # Unrecognized reflection pattern +dotnet_diagnostic.IL2060.severity = error # Unrecognized value passed to parameter with DynamicallyAccessedMembersAttribute +dotnet_diagnostic.IL2061.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2062.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2063.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2064.severity = error # Value assigned to field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2065.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2066.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2067.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2068.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2069.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2070.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2071.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2072.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2073.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2074.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2075.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2076.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2077.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2078.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2079.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2080.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2081.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2082.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2083.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2084.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2085.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2087.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2088.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2089.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2090.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2091.severity = error # Target generic argument doesn't satisfy 'DynamicallyAccessedMembersAttribute' requirements +dotnet_diagnostic.IL2092.severity = error # Value passed to generic parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2093.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2094.severity = error # DynamicallyAccessedMembers on 'this' parameter doesn't match overridden member +dotnet_diagnostic.IL2095.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2096.severity = error # Calling method on statically typed generic instance requires unreferenced code +dotnet_diagnostic.IL2097.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2098.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2099.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2100.severity = error # XML stream doesn't conform to the schema +dotnet_diagnostic.IL2101.severity = error # Embedded XML in assembly couldn't be loaded +dotnet_diagnostic.IL2102.severity = error # Invalid warning number passed to UnconditionalSuppressMessage +dotnet_diagnostic.IL2103.severity = error # Value passed to the 'propertyAccessExpression' parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements +dotnet_diagnostic.IL2104.severity = error # Assembly that was specified through a custom step +dotnet_diagnostic.IL2105.severity = error # Type from a custom step that couldn't be loaded +dotnet_diagnostic.IL2106.severity = error # Method from a custom step that couldn't be loaded +dotnet_diagnostic.IL2107.severity = error # Methods in types that derive from RemotingClientProxy cannot be statically determined +dotnet_diagnostic.IL2108.severity = error # Invalid scope for UnconditionalSuppressMessage +dotnet_diagnostic.IL2109.severity = error # Method doesn't have matching DynamicallyAccessedMembers annotation +dotnet_diagnostic.IL2110.severity = error # Invalid member name in UnconditionalSuppressMessage +dotnet_diagnostic.IL2111.severity = error # Method with parameters or return value with DynamicallyAccessedMembersAttribute is not supported +dotnet_diagnostic.IL2112.severity = error # Reflection call to method with DynamicallyAccessedMembersAttribute requirements cannot be statically analyzed +dotnet_diagnostic.IL2113.severity = error # DynamicallyAccessedMembers on type references Type.MakeGenericType with different requirements +dotnet_diagnostic.IL2114.severity = error # DynamicallyAccessedMembers mismatch on signature types +dotnet_diagnostic.IL2115.severity = error # DynamicallyAccessedMembers on type or base types references member which requires unreferenced code +dotnet_diagnostic.IL2116.severity = error # DynamicallyAccessedMembers on parameter types doesn't match overridden parameter +dotnet_diagnostic.IL2117.severity = error # Methods with DynamicallyAccessedMembersAttribute annotations cannot be replaced +dotnet_diagnostic.IL2122.severity = error # Reflection call to method with UnreferencedCode attribute cannot be statically analyzed +dotnet_diagnostic.IL2123.severity = error # DynamicallyAccessedMembers on method or parameter doesn't match overridden member + +# Native AOT +# See: https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/warnings/ +dotnet_diagnostic.IL3050.severity = error # Using member annotated with RequiresDynamicCode +dotnet_diagnostic.IL3051.severity = error # RequiresDynamicCode attribute is only supported on methods and constructors +dotnet_diagnostic.IL3052.severity = error # RequiresDynamicCode attribute on type is not supported +dotnet_diagnostic.IL3053.severity = error # Assembly has RequiresDynamicCode attribute +dotnet_diagnostic.IL3054.severity = error # Generic expansion in type requires dynamic code +dotnet_diagnostic.IL3055.severity = error # MakeGenericType on non-supported type requires dynamic code +dotnet_diagnostic.IL3056.severity = error # MakeGenericMethod on non-supported method requires dynamic code +dotnet_diagnostic.IL3057.severity = error # Reflection access to generic parameter requires dynamic code + +################### +# SonarAnalyzer.CSharp (S) +################### +# Repository suppressions +dotnet_diagnostic.S1075.severity = none # Hardcoded URI — canonical SourceLink hosts are the point +dotnet_diagnostic.S2436.severity = none # Too many generic parameters — needed for the projector overload +dotnet_diagnostic.S4036.severity = none # PATH-relative process spawn — benchmark only, trusted env +dotnet_diagnostic.S8969.severity = none # Nullability inference is inconsistent across the repository's target frameworks + +# Blocker bugs +dotnet_diagnostic.S1048.severity = none # Finalizers should not throw exceptions — covered by SST1485 +dotnet_diagnostic.S2190.severity = none # Loops and recursions should not be infinite +dotnet_diagnostic.S2275.severity = none # Composite format strings should not lead to unexpected behavior at runtime - DUPLICATE CA2241 +dotnet_diagnostic.S2857.severity = none # SQL keywords should be delimited by whitespace — covered by SST2470 +dotnet_diagnostic.S2930.severity = none # "IDisposables" should be disposed — covered by SST2410 +dotnet_diagnostic.S2931.severity = none # Classes with "IDisposable" members should implement "IDisposable" — covered by SST2315 +dotnet_diagnostic.S3464.severity = none # Type inheritance should not be recursive — covered by SST2437 +dotnet_diagnostic.S3869.severity = none # "SafeHandle.DangerousGetHandle" should not be called -> replaced by SST2484 +dotnet_diagnostic.S3889.severity = none # "Thread.Resume" and "Thread.Suspend" should not be used -> replaced by obsolete or compiler +dotnet_diagnostic.S4159.severity = none # Classes should implement their "ExportAttribute" interfaces — covered by SST2472 + +# Critical bugs +dotnet_diagnostic.S2551.severity = none # Shared resources should not be used for locking — covered by SST1902 +dotnet_diagnostic.S2952.severity = none # Classes should "Dispose" of members from the classes' own "Dispose" methods -> replaced by SST2315 +dotnet_diagnostic.S3449.severity = none # Right operands of shift operators should be integers -> replaced by SST1478 +dotnet_diagnostic.S4275.severity = none # Getters and setters should access the expected fields — covered by SST2422 +dotnet_diagnostic.S4277.severity = none # "Shared" parts should not be created with "new" — covered by SST2473 +dotnet_diagnostic.S4583.severity = none # Calls to delegate's method "BeginInvoke" should be paired with calls to "EndInvoke" -> replaced by obsolete (APM BeginInvoke/EndInvoke) +dotnet_diagnostic.S4586.severity = none # Non-async "Task/Task" methods should not return null — covered by PSH1312 +dotnet_diagnostic.S5856.severity = none # Regular expressions should be syntactically valid — covered by SST2444 +dotnet_diagnostic.S6674.severity = none # Log message template should be syntactically correct — covered by SST2441 + +# Major bugs +dotnet_diagnostic.S1244.severity = none # Floating point numbers should not be tested for equality — covered by SST1473 +dotnet_diagnostic.S1656.severity = none # Variables should not be self-assigned — covered by SST1189 +dotnet_diagnostic.S1751.severity = none # Loops with at most one iteration should be refactored - covered by SST1444 +dotnet_diagnostic.S1764.severity = none # Identical expressions should not be used on both sides of operators — covered by SST1474 +dotnet_diagnostic.S1848.severity = none # Objects should not be created to be dropped immediately without being used -> replaced by SST1480 +dotnet_diagnostic.S1862.severity = none # Related "if/else if" statements should not have the same condition — covered by SST1475 +dotnet_diagnostic.S2114.severity = none # Collections should not be passed as arguments to their own methods — covered by SST2419 +dotnet_diagnostic.S2123.severity = none # Values should not be uselessly incremented -> replaced by SST2222 +dotnet_diagnostic.S2201.severity = none # Methods without side effects should not have their return values ignored — covered by SST2418 +dotnet_diagnostic.S2225.severity = none # "ToString()" method should not return null — covered by SST2431 +dotnet_diagnostic.S2251.severity = none # A "for" loop update clause should move the counter in the right direction — covered by SST2412 +dotnet_diagnostic.S2252.severity = none # For-loop conditions should be true at least once — covered by SST2413 +dotnet_diagnostic.S2445.severity = none # Blocks should be synchronized on read-only fields — covered by SST1904 +dotnet_diagnostic.S2688.severity = none # "NaN" should not be used in comparisons — covered by SST1473 +dotnet_diagnostic.S2757.severity = none # Non-existent operators like "=+" should not be used — covered by SST2417 +dotnet_diagnostic.S2761.severity = none # Doubled prefix operators "!!" and "~~" should not be used — covered by SST1190 +dotnet_diagnostic.S2995.severity = none # "Object.ReferenceEquals" should not be used for value types -> replaced by CA2013 +dotnet_diagnostic.S2996.severity = none # "ThreadStatic" fields should not be initialized -> replaced by CA2019 +dotnet_diagnostic.S2997.severity = none # "IDisposables" created in a "using" statement should not be returned — covered by SST2423 +dotnet_diagnostic.S3005.severity = none # "ThreadStatic" should not be used on non-static fields -> replaced by CA2259 +dotnet_diagnostic.S3168.severity = none # "async" methods should not return "void" — covered by SST1905 +dotnet_diagnostic.S3172.severity = none # Delegates should not be subtracted — covered by SST2448 +dotnet_diagnostic.S3244.severity = none # Anonymous delegates should not be used to unsubscribe from Events — covered by SST2449 +dotnet_diagnostic.S3249.severity = none # Covered by SST1447 (canonical) +dotnet_diagnostic.S3263.severity = none # Static fields should appear in the order they must be initialized — covered by SST2428 +dotnet_diagnostic.S3343.severity = none # Caller information parameters should come at the end of the parameter list — covered by SST2433 +dotnet_diagnostic.S3346.severity = none # Expressions used in "Debug.Assert" should not produce side effects — covered by SST2450 +dotnet_diagnostic.S3453.severity = none # Classes should not have only "private" constructors — covered by SST2451 +dotnet_diagnostic.S3466.severity = none # Optional parameters should be passed to "base" calls — covered by SST2425 +dotnet_diagnostic.S3598.severity = none # One-way "OperationContract" methods should have "void" return type -> replaced by obsolete (WCF) +dotnet_diagnostic.S3603.severity = none # Methods with "Pure" attribute should return a value — covered by SST2452 +dotnet_diagnostic.S3610.severity = none # Nullable type comparison should not be redundant -> replaced by compiler CS0472 +dotnet_diagnostic.S3903.severity = none # Types should be defined in named namespaces — covered by SST2312 +dotnet_diagnostic.S3923.severity = none # All branches in a conditional structure should not have exactly the same implementation — covered by SST1476 +dotnet_diagnostic.S3926.severity = none # Deserialization methods should be provided for "OptionalField" members -> replaced by obsolete (legacy binary serialization) +dotnet_diagnostic.S3927.severity = none # Serialization event handlers should be implemented correctly — covered by SST2430 +dotnet_diagnostic.S3981.severity = none # Collection sizes and array length comparisons should make sense — covered by SST1479 +dotnet_diagnostic.S3984.severity = none # Exceptions should not be created without being thrown — covered by SST1480 +dotnet_diagnostic.S4143.severity = none # Collection elements should not be replaced unconditionally — covered by SST1487 +dotnet_diagnostic.S4210.severity = none # Windows Forms entry points should be marked with STAThread -> replaced by SST2706 +dotnet_diagnostic.S4260.severity = none # "ConstructorArgument" parameters should exist in constructors -> replaced by SST2487 +dotnet_diagnostic.S4428.severity = none # "PartCreationPolicyAttribute" should be used with "ExportAttribute" — covered by SST2474 +dotnet_diagnostic.S6507.severity = none # Blocks should not be synchronized on local variables — covered by SST1903 +dotnet_diagnostic.S6677.severity = none # Message template placeholders should be unique — covered by SST2442 +dotnet_diagnostic.S6797.severity = none # Blazor query parameter type should be supported -> replaced by SST2702 +dotnet_diagnostic.S6798.severity = none # [JSInvokable] attribute should only be used on public methods -> replaced by SST2701 +dotnet_diagnostic.S6800.severity = none # Component parameter type should match the route parameter type constraint -> replaced by SST2703 +dotnet_diagnostic.S6930.severity = none # Backslash should be avoided in route templates -> replaced by SST2700 + +# Minor bugs +dotnet_diagnostic.S1206.severity = none # "Equals(Object)" and "GetHashCode()" should be overridden in pairs - DUPLICATE CA2218 +dotnet_diagnostic.S1226.severity = none # Method parameters, caught exceptions and foreach variables' initial values should not be ignored +dotnet_diagnostic.S2183.severity = none # Integral numbers should not be shifted by zero or more than their number of bits-1 — covered by SST1478 +dotnet_diagnostic.S2184.severity = none # Results of integer division should not be assigned to floating point variables — covered by SST1477 +dotnet_diagnostic.S2328.severity = none # "GetHashCode" should not reference mutable fields — covered by SST1482 +dotnet_diagnostic.S2345.severity = none # Flags enumerations should explicitly initialize all their members — covered by SST2303 +dotnet_diagnostic.S2674.severity = none # The length returned from a stream read should be checked — covered by SST2446 +dotnet_diagnostic.S2934.severity = none # Property assignments should not be made for "readonly" fields not constrained to reference types — covered by SST2421 +dotnet_diagnostic.S2955.severity = none # Generic parameters not constrained to reference types should not be compared to "null" -> replaced by compiler CS0019/CS0037 +dotnet_diagnostic.S3363.severity = none # Date and time should not be used as a type for primary keys — covered by SST2475 +dotnet_diagnostic.S3397.severity = none # "base.Equals" should not be used to check for reference equality in "Equals" if "base" is not "object" — covered by SST2435 +dotnet_diagnostic.S3456.severity = none # "string.ToCharArray()" and "ReadOnlySpan.ToArray()" should not be called redundantly — covered by PSH1217 +dotnet_diagnostic.S3887.severity = none # Mutable, non-private fields should not be "readonly" — covered by SST2322 + +# Blocker vulnerabilities +dotnet_diagnostic.S2115.severity = none # A secure password should be used when connecting to a database -> replaced by SES1203 +dotnet_diagnostic.S2755.severity = none # XML parsers should not be vulnerable to XXE attacks -> replaced by CA3075 +dotnet_diagnostic.S3884.severity = none # "CoSetProxyBlanket" and "CoInitializeSecurity" should not be used -> replaced by obsolete (COM interop security) +dotnet_diagnostic.S6418.severity = none # Secrets should not be hard-coded — covered by SES1201 + +# Critical vulnerabilities +dotnet_diagnostic.S4423.severity = none # Weak SSL/TLS protocols should not be used -> replaced by CA5397/CA5398 +dotnet_diagnostic.S4426.severity = none # Cryptographic keys should be robust -> replaced by CA5385 (RSA) / CA5384 (DSA) +dotnet_diagnostic.S4433.severity = none # LDAP connections should be authenticated -> replaced by SES1310 +dotnet_diagnostic.S4830.severity = none # Server certificates should be verified during SSL/TLS connections -> replaced by SES1102 or SES1108 +dotnet_diagnostic.S5344.severity = none # Passwords should not be stored in plaintext or with a fast hashing algorithm -> replaced by SES1009 +dotnet_diagnostic.S5445.severity = none # Insecure temporary file creation methods should not be used -> replaced by SES1307 +dotnet_diagnostic.S5542.severity = none # Encryption algorithms should be used with secure mode and padding scheme -> replaced by CA5358 +dotnet_diagnostic.S5547.severity = none # Cipher algorithms should be robust -> replaced by CA5351 +dotnet_diagnostic.S5659.severity = none # JWT should be signed and verified with strong cipher algorithms -> replaced by SES1503 + +# Major vulnerabilities +dotnet_diagnostic.S2068.severity = none # Credentials should not be hard-coded -> replaced by SES1201 +dotnet_diagnostic.S2612.severity = none # File permissions should not be set to world-accessible values -> replaced by SES1308 +dotnet_diagnostic.S4211.severity = none # Members should not have conflicting transparency annotations -> replaced by obsolete (Code Access Security) +dotnet_diagnostic.S4212.severity = none # Serialization constructors should be secured -> replaced by obsolete (Code Access Security) +dotnet_diagnostic.S6377.severity = none # XML signatures should be validated securely -> replaced by SES1008 +dotnet_diagnostic.S7039.severity = none # Content Security Policies should be restrictive -> replaced by SES1515 + +# Blocker code smells +dotnet_diagnostic.S1147.severity = none # Exit methods should not be called — covered by SST2321 +dotnet_diagnostic.S1451.severity = none # Track lack of copyright and license headers +dotnet_diagnostic.S2178.severity = none # Short-circuit logic should be used in boolean contexts — covered by SST2415 +dotnet_diagnostic.S2187.severity = none # Test classes should contain at least one test case — covered by SST2504 +dotnet_diagnostic.S2306.severity = none # "async" and "await" should not be used as identifiers -> replaced by compiler (contextual keyword) +dotnet_diagnostic.S2368.severity = none # Public methods should not have multidimensional array parameters — jagged arrays are the chosen layout for hot-path lookup tables +dotnet_diagnostic.S2387.severity = none # Child class fields should not shadow parent class fields — covered by SST1484 +dotnet_diagnostic.S2437.severity = none # Unnecessary bit operations should not be performed — covered by SST1481 +dotnet_diagnostic.S2699.severity = none # Tests should include assertions -> replaced by SST2500 +dotnet_diagnostic.S2953.severity = none # Methods named "Dispose" should implement "IDisposable.Dispose" — covered by SST2316 +dotnet_diagnostic.S2970.severity = none # Assertions should be complete -> replaced by SST2508 +dotnet_diagnostic.S3060.severity = none # "is" should not be used with "this" -> replaced by SST2327 +dotnet_diagnostic.S3237.severity = none # "value" contextual keyword should be used — covered by SST2429 +dotnet_diagnostic.S3427.severity = none # Method overloads with default parameter values should not overlap — covered by SST2319 +dotnet_diagnostic.S3433.severity = none # Test method signatures should be correct -> replaced by SST2509 +dotnet_diagnostic.S3443.severity = none # Type should not be examined on "System.Type" instances — covered by SST2432 +dotnet_diagnostic.S3875.severity = none # "operator==" should not be overloaded on reference types -> replaced by SST2464 +dotnet_diagnostic.S3877.severity = none # Exceptions should not be thrown from unexpected methods — covered by SST1485 +dotnet_diagnostic.S4462.severity = none # Calls to "async" methods should not be blocking - covered by PSH1315 +dotnet_diagnostic.S6422.severity = none # Calls to "async" methods should not be blocking in Azure Functions — covered by PSH1315 +dotnet_diagnostic.S6424.severity = none # Interfaces for durable entities should satisfy the restrictions -> off: Durable Entity-specific; not used in this library + +# Critical code smells +dotnet_diagnostic.S1006.severity = none # Method overrides should not change parameter defaults — covered by SST2424 +dotnet_diagnostic.S1067.severity = none # Expressions should not be too complex +dotnet_diagnostic.S1163.severity = none # Exceptions should not be thrown in finally blocks -> replaced by CA2219 +dotnet_diagnostic.S1186.severity = none # Methods should not be empty — covered by SST1438 +dotnet_diagnostic.S121.severity = none # Control structures should use curly braces (kept over SA1503 — Sonar 20ms vs SA1503 50ms) -> replaced by SST1503 +dotnet_diagnostic.S1215.severity = none # "GC.Collect" should not be called — covered by PSH1021 +dotnet_diagnostic.S126.severity = none # "if ... else if" constructs should end with "else" clauses +dotnet_diagnostic.S131.severity = none # "switch/Select" statements should contain a "default/Case Else" clauses +dotnet_diagnostic.S134.severity = none # Control flow statements "if", "switch", "for", "foreach", "while", "do" and "try" should not be nested too deeply +dotnet_diagnostic.S1541.severity = none # Methods and properties should not be too complex - covered by SST1442 +dotnet_diagnostic.S1699.severity = none # Constructors should only call non-overridable methods — covered by SST1483 +dotnet_diagnostic.S1821.severity = none # "switch" statements should not be nested -> replaced by SST2252 +dotnet_diagnostic.S1944.severity = none # Invalid casts should be avoided -> replaced by compiler CS0030 +dotnet_diagnostic.S1994.severity = none # "for" loop increment clauses should modify the loops' counters — covered by SST2411 +dotnet_diagnostic.S2197.severity = none # Modulus results should not be checked for direct equality — covered by SST2416 +dotnet_diagnostic.S2198.severity = none # Unnecessary mathematical comparisons should not be made -> replaced by SST2489 +dotnet_diagnostic.S2223.severity = none # Non-constant static fields should not be visible - DUPLICATE CA2211 +dotnet_diagnostic.S2290.severity = none # Field-like events should not be virtual -> replaced by SST2456 +dotnet_diagnostic.S2291.severity = none # Overflow checking should not be disabled for "Enumerable.Sum" — covered by SST2457 +dotnet_diagnostic.S2302.severity = none # "nameof" should be used — covered by SST1415 +dotnet_diagnostic.S2330.severity = none # Array covariance should not be used — covered by SST2434 +dotnet_diagnostic.S2339.severity = none # Public constant members should not be used — covered by SST2311 +dotnet_diagnostic.S2346.severity = none # Flags enumerations zero-value members should be named "None" - DUPLICATE CA1008 +dotnet_diagnostic.S2360.severity = none # Optional parameters should not be used — conflicts with SST2433, which owns caller-info parameters requiring a default +dotnet_diagnostic.S2365.severity = none # Properties should not make collection or array copies — covered by PSH1017 +dotnet_diagnostic.S2479.severity = none # Whitespace and control characters in string literals should be explicit — covered by SST1192 +dotnet_diagnostic.S2692.severity = none # "IndexOf" checks should not be for positive numbers — covered by SST2420 +dotnet_diagnostic.S2696.severity = none # Instance members should not write to "static" fields -> replaced by SST2402 +dotnet_diagnostic.S2701.severity = none # Literal boolean values should not be used in assertions — covered by SST2503 +dotnet_diagnostic.S3215.severity = none # "interface" instances should not be cast to concrete types -> deliberately unenforced, same reason as SST2326 +dotnet_diagnostic.S3216.severity = none # "ConfigureAwait(false)" should be used -> replaced by CA2007 +dotnet_diagnostic.S3217.severity = none # "Explicit" conversions of "foreach" loops should not be used — covered by SST2225 +dotnet_diagnostic.S3218.severity = none # Inner class members should not shadow outer class "static" or type members — covered by SST1484 +dotnet_diagnostic.S3265.severity = none # Non-flags enums should not be used in bitwise operations — covered by SST2458 +dotnet_diagnostic.S3353.severity = none # Unchanged variables should be marked as "const" — covered by PSH1402 +dotnet_diagnostic.S3447.severity = none # "[Optional]" should not be used on "ref" or "out" parameters — covered by SST2459 +dotnet_diagnostic.S3451.severity = none # "[DefaultValue]" should not be used when "[DefaultParameterValue]" is meant — covered by SST2460 +dotnet_diagnostic.S3600.severity = none # "params" should not be introduced on overrides — covered by SST2426 +dotnet_diagnostic.S3776.severity = none # Cognitive Complexity of methods should not be too high - covered by SST1443 +dotnet_diagnostic.S3871.severity = none # Exception types should be "public" -> replaced by CA1064 +dotnet_diagnostic.S3874.severity = none # "out" and "ref" parameters — repo idiom is TryX(..., out T value) +dotnet_diagnostic.S3904.severity = none # Assemblies should have version information -> replaced by obsolete (SDK supplies assembly version) +dotnet_diagnostic.S3937.severity = none # Number patterns should be regular — covered by SST1119 +dotnet_diagnostic.S3972.severity = none # Conditionals should start on new lines — covered by SST1146 +dotnet_diagnostic.S3973.severity = none # A conditionally executed single line should be denoted by indentation -> replaced by SST1503 +dotnet_diagnostic.S3998.severity = none # Threads should not lock on objects with weak identity -> replaced by SST1902 +dotnet_diagnostic.S4000.severity = none # Pointers to unmanaged memory should not be visible -> replaced by SST2328 +dotnet_diagnostic.S4015.severity = none # Inherited member visibility should not be decreased — covered by SST2462 +dotnet_diagnostic.S4019.severity = none # Base class methods should not be hidden — covered by SST2427 +dotnet_diagnostic.S4025.severity = none # Child class fields should not differ from parent class fields only by capitalization — covered by SST2463 +dotnet_diagnostic.S4039.severity = none # Interface methods should be callable by derived types - DUPLICATE CA1033 +dotnet_diagnostic.S4487.severity = none # Unread "private" fields should be removed +dotnet_diagnostic.S4524.severity = none # "default" clauses should be first or last — covered by SST1219 +dotnet_diagnostic.S4635.severity = none # Start index should be used instead of calling Substring — covered by PSH1218 +dotnet_diagnostic.S5034.severity = none # "ValueTask" should be consumed correctly — covered by PSH1316 +dotnet_diagnostic.S6967.severity = none # ModelState.IsValid should be called in controller actions -> off: ASP.NET MVC-specific; no controllers in this library +dotnet_diagnostic.S8367.severity = none # Identifiers should not conflict with the C# 14 "field" contextual keyword - the C# 14 compiler reports this: CS9273 (error) for a local named field in an accessor, CS9258 (warning) for a rebinding read +dotnet_diagnostic.S8368.severity = none # Identifiers should not conflict with the C# 14 "extension" contextual keyword - the C# 14 compiler reports this as CS9306, and SST1300 already flags the lowercase type name +dotnet_diagnostic.S8380.severity = none # Return types named "partial" should be escaped with "@" - the compiler reports this as CS8981, and SST1300 already flags the lowercase type name +dotnet_diagnostic.S8381.severity = none # "scoped" should be escaped when used as an identifier or type name in parenthesized lambda parameter lists -> replaced by compiler +dotnet_diagnostic.S927.severity = none # Parameter names should match base declaration and other partial definitions - DUPLICATE CA1725 + +# Major code smells +dotnet_diagnostic.S103.severity = none # Lines should not be too long — covered by SST1521 +dotnet_diagnostic.S104.severity = none # Files should not have too many lines of code — covered by SST1522 +dotnet_diagnostic.S106.severity = none # Covered by SST1449 (canonical) +dotnet_diagnostic.S1066.severity = none # Mergeable "if" statements should be combined — covered by SST2013 +dotnet_diagnostic.S107.severity = none # Methods should not have too many parameters — covered by SST1472 +dotnet_diagnostic.S108.severity = none # Nested blocks of code should not be left empty — covered by SST1439 +dotnet_diagnostic.S109.severity = none # Magic numbers should not be used — covered by SST1471 +dotnet_diagnostic.S110.severity = none # Inheritance tree of classes should not be too deep — covered by SST1446 +dotnet_diagnostic.S1110.severity = none # Redundant pairs of parentheses should be removed — covered by SST1459 +dotnet_diagnostic.S1117.severity = none # Local variables should not shadow class fields or properties — covered by SST1484 +dotnet_diagnostic.S1118.severity = none # Utility classes should not have public constructors - DUPLICATE CA1052 +dotnet_diagnostic.S112.severity = none # General or reserved exceptions should never be thrown — covered by SST2409 +dotnet_diagnostic.S1121.severity = none # Assignments should not be made from within sub-expressions — covered by SST1187 +dotnet_diagnostic.S1123.severity = none # "Obsolete" attributes should include explanations — covered by SST2308 +dotnet_diagnostic.S1134.severity = none # Track uses of "FIXME" tags -> off: TODO comment tracker; not enforced here +dotnet_diagnostic.S1144.severity = none # Covered by SST1440 (canonical) +dotnet_diagnostic.S1151.severity = none # "switch case" clauses should not have too many lines of code — covered by SST1524 +dotnet_diagnostic.S1168.severity = none # Empty arrays and collections should be returned instead of null — covered by SST2306 +dotnet_diagnostic.S1172.severity = none # Unused method parameters should be removed — covered by SST1461 +dotnet_diagnostic.S1200.severity = none # Classes should not be coupled to too many other classes +dotnet_diagnostic.S122.severity = none # Statements should be on separate lines — covered by SST1107 +dotnet_diagnostic.S125.severity = none # Sections of code should not be commented out — covered by SST1148 +dotnet_diagnostic.S127.severity = none # "for" loop stop conditions should be invariant — covered by SST2465 +dotnet_diagnostic.S138.severity = none # Functions should not have too many lines of code — covered by SST1523 +dotnet_diagnostic.S1479.severity = none # "switch" statements with many "case" clauses — covered by SST1423 +dotnet_diagnostic.S1607.severity = none # Tests should not be ignored -> off: ignored-test tracker; not enforced here +dotnet_diagnostic.S1696.severity = none # NullReferenceException should not be caught — covered by SST2401 +dotnet_diagnostic.S1854.severity = none # Unused assignments should be removed - DUPLICATE IDE0059 +dotnet_diagnostic.S1871.severity = none # Two branches in a conditional structure should not have exactly the same implementation — covered by SST2414 +dotnet_diagnostic.S2139.severity = none # Exceptions should be either logged or rethrown but not both -> replaced by SST2488 +dotnet_diagnostic.S2166.severity = none # Classes named like "Exception" should extend "Exception" or a subclass - DUPLICATE CA1710 +dotnet_diagnostic.S2234.severity = none # Arguments should be passed in the same order as the method parameters -> replaced by SST2400 +dotnet_diagnostic.S2326.severity = none # Covered by SST1452 (canonical) +dotnet_diagnostic.S2327.severity = none # "try" statements with identical "catch" and/or "finally" blocks should be merged -> replaced by SST2490 +dotnet_diagnostic.S2357.severity = none # Fields should be private — duplicate of SST1401 (canonical); fields intentionally exposed (e.g. public test fields for reflection) already carry per-site SST1401 suppressions +dotnet_diagnostic.S2372.severity = none # Exceptions should not be thrown from property getters — covered by SST1485 +dotnet_diagnostic.S2376.severity = none # Write-only properties should not be used — covered by SST1421 +dotnet_diagnostic.S2629.severity = none # Logging templates should be constant -> replaced by CA2254 +dotnet_diagnostic.S2681.severity = none # Multiline blocks should be enclosed in curly braces — covered by SST1503 +dotnet_diagnostic.S2743.severity = none # Static fields should not be used in generic types - DUPLICATE CA1000 — covered by SST1431 +dotnet_diagnostic.S2925.severity = none # "Thread.Sleep" should not be used in tests — covered by SST2506 +dotnet_diagnostic.S2933.severity = none # Fields that are only assigned in the constructor should be "readonly" - DUPLICATE IDE0044 +dotnet_diagnostic.S2971.severity = none # LINQ expressions should be simplified — covered by PSH1101/PSH1102 +dotnet_diagnostic.S3010.severity = none # Static fields should not be updated in constructors — covered by SST2402 +dotnet_diagnostic.S3011.severity = none # Reflection should not be used to increase accessibility of classes, methods, or fields -> replaced by SES1406 +dotnet_diagnostic.S3059.severity = none # Types should not have members with visibility set higher than the type's visibility +dotnet_diagnostic.S3063.severity = none # "StringBuilder" data should be used — covered by SST2408 +dotnet_diagnostic.S3169.severity = none # Multiple "OrderBy" calls should not be used — covered by PSH1108 +dotnet_diagnostic.S3246.severity = none # Generic type parameters should be co/contravariant when possible -> off: low-precision variance suggestion; not enforced +dotnet_diagnostic.S3262.severity = none # "params" should be used on overrides — covered by SST2426 +dotnet_diagnostic.S3264.severity = none # Events should be invoked — covered by SST2407 +dotnet_diagnostic.S3358.severity = none # Ternary operators should not be nested — covered by SST1147 +dotnet_diagnostic.S3366.severity = none # "this" should not be exposed from constructors — covered by SST2403 +dotnet_diagnostic.S3415.severity = none # Assertion arguments should be passed in the correct order — covered by SST2502 +dotnet_diagnostic.S3431.severity = none # "[ExpectedException]" should not be used — covered by SST2507 +dotnet_diagnostic.S3442.severity = none # "abstract" classes should not have "public" constructors — covered by SST1428 +dotnet_diagnostic.S3445.severity = none # Exceptions should not be explicitly rethrown — covered by SST1430 +dotnet_diagnostic.S3457.severity = none # Composite format strings should be used correctly — covered by SST1454 +dotnet_diagnostic.S3597.severity = none # "ServiceContract" and "OperationContract" attributes should be used together -> replaced by obsolete (WCF) +dotnet_diagnostic.S3880.severity = none # Finalizers should not be empty — covered by PSH1002 +dotnet_diagnostic.S3881.severity = none # "IDisposable" should be implemented correctly — covered by SST2300 +dotnet_diagnostic.S3885.severity = none # "Assembly.Load" should be used -> replaced by SST2486 +dotnet_diagnostic.S3898.severity = none # Covered by PSH1005 (canonical) +dotnet_diagnostic.S3902.severity = none # Covered by PSH1404 (canonical) +dotnet_diagnostic.S3906.severity = none # Event Handlers should have the correct signature — covered by SST2304 +dotnet_diagnostic.S3908.severity = none # Generic event handlers should be used -> replaced by SST2304 +dotnet_diagnostic.S3909.severity = none # Collections should implement the generic interface -> replaced by CA1010 +dotnet_diagnostic.S3925.severity = none # "ISerializable" should be implemented correctly - BinaryFormatter / ISerializable serialization is obsoleted (SYSLIB0050/0051) in modern .NET; we do not opt into legacy serialization for any exception type +dotnet_diagnostic.S3928.severity = none # Parameter names used into ArgumentException constructors should match an existing one - DUPLICATE CA2208 +dotnet_diagnostic.S3956.severity = none # "Generic.List" instances should not be part of public APIs +dotnet_diagnostic.S3971.severity = none # "GC.SuppressFinalize" should not be called — conflicts with PSH1008, which owns the pointless-SuppressFinalize direction and exempts unsealed types +dotnet_diagnostic.S3990.severity = none # Assemblies should be marked as CLS compliant -> replaced by CA1014 +dotnet_diagnostic.S3992.severity = none # Assemblies should explicitly specify COM visibility -> replaced by CA1017 +dotnet_diagnostic.S3993.severity = none # Custom attributes should be marked with "System.AttributeUsageAttribute" -> replaced by CA1018 +dotnet_diagnostic.S3994.severity = none # URI Parameters should not be strings +dotnet_diagnostic.S3995.severity = none # URI return values should not be strings +dotnet_diagnostic.S3996.severity = none # URI properties should not be strings +dotnet_diagnostic.S3997.severity = none # String URI overloads should call "System.Uri" overloads -> replaced by CA1054/CA1056/CA1057 +dotnet_diagnostic.S4002.severity = none # Disposable types should declare finalizers — covered by SST2317 +dotnet_diagnostic.S4004.severity = none # Collection properties should be readonly — covered by SST2305 +dotnet_diagnostic.S4005.severity = none # "System.Uri" arguments should be used instead of strings +dotnet_diagnostic.S4016.severity = none # Enumeration members should not be named "Reserved" -> replaced by CA1700 +dotnet_diagnostic.S4017.severity = none # Method signatures should not contain nested generic types +dotnet_diagnostic.S4035.severity = none # Classes implementing "IEquatable" should be sealed — covered by SST2301 +dotnet_diagnostic.S4050.severity = none # Operators should be overloaded consistently — covered by SST2302 +dotnet_diagnostic.S4055.severity = none # Literals should not be passed as localized parameters +dotnet_diagnostic.S4057.severity = none # Locales should be set for data types -> replaced by obsolete +dotnet_diagnostic.S4059.severity = none # Property names should not match get methods - DUPLICATE CA1721 +dotnet_diagnostic.S4070.severity = none # Non-flags enums should not be marked with "FlagsAttribute" — covered by SST2303 +dotnet_diagnostic.S4144.severity = none # Methods should not have identical implementations — covered by SST2318 +dotnet_diagnostic.S4200.severity = none # Native methods should be wrapped -> replaced by CA1401 +dotnet_diagnostic.S4214.severity = none # "P/Invoke" methods should not be visible - DUPLICATE CA1401 +dotnet_diagnostic.S4220.severity = none # Events should have proper arguments — covered by SST2436 +dotnet_diagnostic.S4456.severity = none # Parameter validation in yielding methods should be wrapped — covered by SST2404 +dotnet_diagnostic.S4457.severity = none # Parameter validation in "async"/"await" methods should be wrapped — covered by SST2325 +dotnet_diagnostic.S4545.severity = none # "DebuggerDisplayAttribute" strings should reference existing members — covered by SST2405 +dotnet_diagnostic.S4581.severity = none # "new Guid()" should not be used — covered by SST2012 +dotnet_diagnostic.S6354.severity = none # Use a testable date/time provider — covered by SST2010 +dotnet_diagnostic.S6419.severity = none # Azure Functions should be stateless -> off: Azure Functions-specific; not used in this library +dotnet_diagnostic.S6420.severity = none # Client instances should not be recreated on each Azure Function invocation — covered by PSH1418 +dotnet_diagnostic.S6421.severity = none # Azure Functions should use Structured Error Handling -> off: Azure Functions-specific; not used in this library +dotnet_diagnostic.S6423.severity = none # Azure Functions should log all failures -> off: Azure Functions-specific; not used in this library +dotnet_diagnostic.S6561.severity = none # Avoid using "DateTime.Now" for benchmarking or timing operations — covered by PSH1408 +dotnet_diagnostic.S6562.severity = none # Covered by SST1451 (canonical) +dotnet_diagnostic.S6563.severity = none # Use UTC when recording DateTime instants — covered by SST2011 +dotnet_diagnostic.S6566.severity = none # Use "DateTimeOffset" instead of "DateTime" — covered by SST2016 +dotnet_diagnostic.S6575.severity = none # Use "TimeZoneInfo.FindSystemTimeZoneById" without converting the timezones with "TimezoneConverter" -> replaced by PSH1419 +dotnet_diagnostic.S6580.severity = none # Use a format provider when parsing date and time - DUPLICATE CA1305 +dotnet_diagnostic.S6673.severity = none # Log message template placeholders should be in the right order — covered by SST2440 +dotnet_diagnostic.S6802.severity = none # Using lambda expressions in loops should be avoided in Blazor markup section -> replaced by PSH1600 +dotnet_diagnostic.S6803.severity = none # Parameters with SupplyParameterFromQuery attribute should be used only in routable components -> off: Blazor-specific; no Blazor surface in this library +dotnet_diagnostic.S6931.severity = none # ASP.NET controller actions should not have a route template starting with "/" -> off: ASP.NET MVC-specific; no controllers in this library +dotnet_diagnostic.S6932.severity = none # Use model binding instead of reading raw request data -> off: ASP.NET MVC-specific; no controllers in this library +dotnet_diagnostic.S6934.severity = none # A Route attribute should be added to the controller when a route template is specified at the action level -> off: ASP.NET MVC-specific; no controllers in this library +dotnet_diagnostic.S6960.severity = none # Controllers should not have mixed responsibilities -> off: ASP.NET MVC-specific; no controllers in this library +dotnet_diagnostic.S6961.severity = none # API Controllers should derive from ControllerBase instead of Controller -> off: ASP.NET MVC-specific; no controllers in this library +dotnet_diagnostic.S6962.severity = none # You should pool HTTP connections with HttpClientFactory — covered by PSH1418 +dotnet_diagnostic.S6964.severity = none # Value type property used as input in a controller action should be nullable, required or annotated with the JsonRequiredAttribute to avoid under-posting. -> replaced by SST2705 (opt-in) +dotnet_diagnostic.S6965.severity = none # REST API actions should be annotated with an HTTP verb attribute -> replaced by SST2704 +dotnet_diagnostic.S6966.severity = none # Awaitable method should be used — covered by PSH1313 +dotnet_diagnostic.S6968.severity = none # Actions that return a value should be annotated with ProducesResponseTypeAttribute containing the return type -> off: ASP.NET MVC-specific; no controllers in this library +dotnet_diagnostic.S881.severity = none # Increment (++) and decrement (--) operators should not be used in a method call or mixed with other operators in an expression — covered by SST2015 +dotnet_diagnostic.S907.severity = none # "goto" statement should not be used — covered by SST2014 + +# Minor code smells +dotnet_diagnostic.S100.severity = none # Methods and properties should be named in PascalCase — covered by SST1300 +dotnet_diagnostic.S101.severity = none # Types should be named in PascalCase — covered by SST1300 +dotnet_diagnostic.S105.severity = none # Tabulation characters should not be used — covered by SST1027 +dotnet_diagnostic.S1104.severity = none # Fields should not have public accessibility - DUPLICATE CA1051 +dotnet_diagnostic.S1109.severity = none # A close curly brace should be located at the beginning of a line — covered by SST1500 +dotnet_diagnostic.S1116.severity = none # Empty statements should be removed - DUPLICATE SA1106 +dotnet_diagnostic.S1125.severity = none # Boolean literals should not be redundant — covered by SST1182 +dotnet_diagnostic.S1128.severity = none # Covered by SST1445 (canonical) +dotnet_diagnostic.S113.severity = none # Files should end with a newline +dotnet_diagnostic.S1155.severity = none # "Any()" should be used to test for emptiness — covered by PSH1119 +dotnet_diagnostic.S1185.severity = none # Overriding members should do more than simply call the same member in the base class - DUPLICATE RCS1132 +dotnet_diagnostic.S1192.severity = none # String literals should not be duplicated — covered by SST1486 +dotnet_diagnostic.S1199.severity = none # Nested code blocks should not be used — covered by SST1138 +dotnet_diagnostic.S1210.severity = none # "Equals" and the comparison operators should be overridden when implementing "IComparable" - DUPLICATE CA1036 +dotnet_diagnostic.S1227.severity = none # break statements should not be used except for switch cases +dotnet_diagnostic.S1264.severity = none # A "while" loop should be used instead of a "for" loop — covered by SST2245 +dotnet_diagnostic.S1301.severity = none # "switch" statements should have at least 3 "case" clauses +dotnet_diagnostic.S1312.severity = none # Logger fields should be "private static readonly" +dotnet_diagnostic.S1449.severity = none # Culture should be specified for "string" operations — covered by PSH1207 +dotnet_diagnostic.S1450.severity = none # Private fields only used as local variables in methods should become local variables — covered by SST1422 +dotnet_diagnostic.S1481.severity = none # Unused local variables should be removed — covered by SST1497 +dotnet_diagnostic.S1643.severity = none # Covered by PSH1206 (canonical) +dotnet_diagnostic.S1659.severity = none # Multiple variables should not be declared on the same line — covered by SST1132 +dotnet_diagnostic.S1694.severity = none # An abstract class should have both abstract and concrete methods — covered by SST2323 +dotnet_diagnostic.S1698.severity = none # "==" should not be used when "Equals" is overridden — covered by SST1495 +dotnet_diagnostic.S1858.severity = none # "ToString()" calls should not be redundant — covered by PSH1211 +dotnet_diagnostic.S1905.severity = none # Redundant casts should not be used - DUPLICATE IDE0004 — covered by SST1175 +dotnet_diagnostic.S1939.severity = none # Inheritance list should not be redundant — covered by SST1490 and SST1177 +dotnet_diagnostic.S1940.severity = none # Boolean checks should not be inverted — covered by SST1172 +dotnet_diagnostic.S2094.severity = none # Classes should not be empty — covered by SST1436 +dotnet_diagnostic.S2148.severity = none # Underscores should be used to make large numbers readable — covered by SST1191 +dotnet_diagnostic.S2156.severity = none # "sealed" classes should not have "protected" members — covered by SST1427 +dotnet_diagnostic.S2219.severity = none # Runtime type checking should be simplified — covered by SST2007 +dotnet_diagnostic.S2221.severity = none # "Exception" should not be caught +dotnet_diagnostic.S2292.severity = none # Trivial properties should be auto-implemented — covered by SST1420 +dotnet_diagnostic.S2325.severity = none # Methods and properties that don't access instance data should be static - DUPLICATE CA1822 +dotnet_diagnostic.S2333.severity = none # Redundant modifiers should not be used — covered by SST1419/SST1491 +dotnet_diagnostic.S2342.severity = none # Enumeration types should comply with a naming convention — covered by SST1319 +dotnet_diagnostic.S2344.severity = none # Enumeration type names should not have "Flags" or "Enum" suffixes - DUPLICATE CA1711 +dotnet_diagnostic.S2386.severity = none # Mutable fields should not be "public static" — covered by SST1499 +dotnet_diagnostic.S2486.severity = none # Generic exceptions should not be ignored — covered by SST1429 +dotnet_diagnostic.S2737.severity = none # "catch" clauses should do more than rethrow — covered by SST1470 +dotnet_diagnostic.S2760.severity = none # Sequential tests should not check the same condition — covered by SST1475 +dotnet_diagnostic.S3052.severity = none # Covered by PSH1403 (canonical) +dotnet_diagnostic.S3220.severity = none # Method calls should not resolve ambiguously to overloads with "params" — covered by SST2467 +dotnet_diagnostic.S3234.severity = none # Covered by PSH1008 (canonical) +dotnet_diagnostic.S3235.severity = none # Redundant parentheses should not be used — covered by SST1459 +dotnet_diagnostic.S3236.severity = none # Covered by SST1448 (canonical) +dotnet_diagnostic.S3240.severity = none # The simplest possible condition syntax should be used - duplicate of SST1198 +dotnet_diagnostic.S3241.severity = none # Methods should not return values that are never used -> off: needs whole-program analysis; not enforced here +dotnet_diagnostic.S3242.severity = none # Method parameters should be declared with base types +dotnet_diagnostic.S3247.severity = none # Duplicate casts should not be made — covered by SST1175 +dotnet_diagnostic.S3251.severity = none # Implementations should be provided for "partial" methods — covered by SST2468 +dotnet_diagnostic.S3253.severity = none # Constructor and destructor declarations should not be redundant — covered by SST1433 +dotnet_diagnostic.S3254.severity = none # Default parameter values should not be passed as arguments — covered by SST1494 +dotnet_diagnostic.S3256.severity = none # "string.IsNullOrEmpty" should be used — covered by PSH1204 (style configurable) +dotnet_diagnostic.S3257.severity = none # Declarations and initializations should be as concise as possible -> replaced by SST2202 +dotnet_diagnostic.S3260.severity = none # Non-derived "private" classes and records should be "sealed" — covered by PSH1411 +dotnet_diagnostic.S3261.severity = none # Namespaces should not be empty — covered by SST1435 +dotnet_diagnostic.S3267.severity = none # Loops should be simplified with "LINQ" expressions +dotnet_diagnostic.S3376.severity = none # Attribute, EventArgs, and Exception type names should end with the type being extended - DUPLICATE CA1710 +dotnet_diagnostic.S3398.severity = none # "private" methods called only by inner classes should be moved to those classes — covered by SST1498 +dotnet_diagnostic.S3400.severity = none # Methods should not return constants — covered by SST1493 +dotnet_diagnostic.S3416.severity = none # Loggers should be named for their enclosing types — covered by SST2443 +dotnet_diagnostic.S3440.severity = none # Variables should not be checked against the values they're about to be assigned — covered by SST1492 +dotnet_diagnostic.S3441.severity = none # Redundant property names should be omitted in anonymous classes — covered by SST1173 +dotnet_diagnostic.S3444.severity = none # Interfaces should not simply inherit from base interfaces with colliding members — covered by SST2320 +dotnet_diagnostic.S3450.severity = none # Parameters with "[DefaultParameterValue]" attributes should also be marked "[Optional]" -> replaced by obsolete (legacy DefaultParameterValue) +dotnet_diagnostic.S3458.severity = none # Empty "case" clauses that fall through to the "default" should be omitted — covered by SST1466 +dotnet_diagnostic.S3459.severity = none # Unassigned members should be removed - the compiler reports this as CS0649; the rule only ever fires on private never-assigned fields +dotnet_diagnostic.S3532.severity = none # Empty "default" clauses should be removed — covered by SST1179 +dotnet_diagnostic.S3604.severity = none # Member initializer values should not be redundant — covered by PSH1403 +dotnet_diagnostic.S3626.severity = none # Jump statements should not be redundant — covered by SST1174 +dotnet_diagnostic.S3717.severity = none # Track use of "NotImplementedException" -> replaced by SST2485 +dotnet_diagnostic.S3872.severity = none # Parameter names should not duplicate the names of their methods -> replaced by SST1320 +dotnet_diagnostic.S3876.severity = none # Strings or integral types should be used for indexers -> replaced by CA1043 +dotnet_diagnostic.S3878.severity = none # Arrays should not be created for params parameters — covered by PSH1018 +dotnet_diagnostic.S3897.severity = none # Classes that provide "Equals()" should implement "IEquatable" -> replaced by CA1067 +dotnet_diagnostic.S3962.severity = none # Covered by PSH1402 (canonical) +dotnet_diagnostic.S3963.severity = none # "static" fields should be initialized inline - DUPLICATE CA1810 +dotnet_diagnostic.S3967.severity = none # Multidimensional arrays should not be used - DUPLICATE CA1814 +dotnet_diagnostic.S4018.severity = none # All type parameters should be used in the parameter list to enable type inference — covered by SST2307 +dotnet_diagnostic.S4022.severity = none # Enumerations should have "Int32" storage — covered by SST2313 +dotnet_diagnostic.S4023.severity = none # Interfaces should not be empty — covered by SST1437 +dotnet_diagnostic.S4026.severity = none # Assemblies should be marked with "NeutralResourcesLanguageAttribute" -> replaced by CA1824 +dotnet_diagnostic.S4027.severity = none # Exceptions should provide standard constructors — covered by SST1488 +dotnet_diagnostic.S4040.severity = none # Strings should be normalized to uppercase - DUPLICATE CA1308 +dotnet_diagnostic.S4041.severity = none # Type names should not match namespaces - DUPLICATE CA1724 +dotnet_diagnostic.S4047.severity = none # Generics should be used when appropriate -> off: fuzzy prefer-generics suggestion; not enforced +dotnet_diagnostic.S4049.severity = none # Properties should be preferred - DUPLICATE CA1024 +dotnet_diagnostic.S4052.severity = none # Types should not extend outdated base types -> replaced by obsolete +dotnet_diagnostic.S4056.severity = none # Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used - DUPLICATE CA1305 +dotnet_diagnostic.S4058.severity = none # Covered by PSH1207 (canonical) +dotnet_diagnostic.S4060.severity = none # Non-abstract attributes should be sealed - DUPLICATE CA1813 +dotnet_diagnostic.S4061.severity = none # "params" should be used instead of "varargs" -> replaced by obsolete (__arglist varargs) +dotnet_diagnostic.S4069.severity = none # Operator overloads should have named alternatives - DUPLICATE CA2225 +dotnet_diagnostic.S4136.severity = none # Method overloads should be grouped together — covered by SST1218 +dotnet_diagnostic.S4201.severity = none # Null checks should not be combined with "is" operator checks — covered by SST2018 +dotnet_diagnostic.S4225.severity = none # Extension methods should not extend "object" — covered by SST1706 +dotnet_diagnostic.S4226.severity = none # Extensions should be in separate namespaces +dotnet_diagnostic.S4261.severity = none # Methods should be named according to their synchronicities - Async suffix not used +dotnet_diagnostic.S4663.severity = none # Covered by SST1120 (canonical) +dotnet_diagnostic.S6513.severity = none # "ExcludeFromCodeCoverage" attributes should include a justification - not available on net462 and older TFMs +dotnet_diagnostic.S6585.severity = none # Don't hardcode the format when turning dates and times to strings — covered by SST2445 +dotnet_diagnostic.S6588.severity = none # Use the "UnixEpoch" field instead of creating "DateTime" instances that point to the beginning of the Unix epoch — covered by PSH1413 +dotnet_diagnostic.S6594.severity = none # Covered by PSH1406 (canonical) +dotnet_diagnostic.S6602.severity = none # Covered by PSH1110 (canonical) +dotnet_diagnostic.S6603.severity = none # Covered by PSH1110 (canonical) +dotnet_diagnostic.S6605.severity = none # Covered by PSH1110 (canonical) +dotnet_diagnostic.S6607.severity = none # The collection should be filtered before sorting — covered by PSH1107 +dotnet_diagnostic.S6608.severity = none # Covered by PSH1106 (canonical) +dotnet_diagnostic.S6609.severity = none # "Min/Max" properties of "Set" types should be used instead of the "Enumerable" extension methods — covered by PSH1122 +dotnet_diagnostic.S6610.severity = none # Covered by PSH1201 (canonical) +dotnet_diagnostic.S6612.severity = none # The lambda parameter should be used instead of capturing arguments in "ConcurrentDictionary" methods — covered by PSH1006 +dotnet_diagnostic.S6613.severity = none # "First" and "Last" properties of "LinkedList" should be used instead of the "First()" and "Last()" extension methods — covered by PSH1124 +dotnet_diagnostic.S6617.severity = none # Covered by PSH1111 (canonical) +dotnet_diagnostic.S6618.severity = none # "string.Create" should be used instead of "FormattableString" — covered by PSH1209 +dotnet_diagnostic.S6664.severity = none # The code block contains too many logging calls -> off: fuzzy too-many-logging-calls metric; not enforced +dotnet_diagnostic.S6667.severity = none # Logging in a catch clause should pass the caught exception as a parameter. — covered by SST2438 +dotnet_diagnostic.S6668.severity = none # Logging arguments should be passed to the correct parameter — covered by SST2439 +dotnet_diagnostic.S6669.severity = none # Logger field or property name should comply with a naming convention -> replaced by SST2601 +dotnet_diagnostic.S6670.severity = none # "Trace.Write" and "Trace.WriteLine" should not be used — covered by SST2600 +dotnet_diagnostic.S6672.severity = none # Generic logger injection should match enclosing type — covered by SST2443 +dotnet_diagnostic.S6675.severity = none # "Trace.WriteLineIf" should not be used with "TraceSwitch" levels -> off: niche TraceSwitch misuse; not enforced here +dotnet_diagnostic.S6678.severity = none # Use PascalCase for named placeholders -> replaced by CA1727 +dotnet_diagnostic.S818.severity = none # Literal suffixes should be upper case — covered by SST2244 + +# Informational code smells +dotnet_diagnostic.S1133.severity = none # Deprecated code should be removed — covered by SST2310 +dotnet_diagnostic.S1135.severity = none # Track uses of "TODO" tags -> off: FIXME comment tracker; not enforced here +dotnet_diagnostic.S1309.severity = none # Track uses of in-source issue suppressions + +# Uncategorized +dotnet_diagnostic.S9999-cpd.severity = error # Copy-paste token calculator +dotnet_diagnostic.S9999-log.severity = error # Log generator +dotnet_diagnostic.S9999-metadata.severity = error # File metadata generator +dotnet_diagnostic.S9999-metrics.severity = error # Metrics calculator +dotnet_diagnostic.S9999-symbolRef.severity = error # Symbol reference calculator +dotnet_diagnostic.S9999-telemetry.severity = error # Telemetry generator +dotnet_diagnostic.S9999-testMethodDeclaration.severity = error # Test method declarations generator +dotnet_diagnostic.S9999-token-type.severity = error # Token type calculator +dotnet_diagnostic.S9999-warning.severity = error # Analysis Warning generator + +# Critical security hotspots +dotnet_diagnostic.S2245.severity = none # Using pseudorandom number generators (PRNGs) is security-sensitive - DUPLICATE CA5394 +dotnet_diagnostic.S2257.severity = none # Using non-standard cryptographic algorithms is security-sensitive -> replaced by SES1007 +dotnet_diagnostic.S4502.severity = none # Disabling CSRF protections is security-sensitive -> replaced by in-box ASP.NET antiforgery analyzer +dotnet_diagnostic.S4790.severity = none # Using weak hashing algorithms is security-sensitive -> replaced by CA5350/CA5351 +dotnet_diagnostic.S4792.severity = none # Configuring loggers is security-sensitive -> off: logging-configuration audit hotspot; not enforced here +dotnet_diagnostic.S5042.severity = none # Expanding archive files without controlling resource consumption is security-sensitive -> off: unbounded decompression; needs taint analysis, out of scope +dotnet_diagnostic.S5332.severity = none # Using clear-text protocols is security-sensitive -> replaced by SES1106 +dotnet_diagnostic.S5443.severity = none # Using publicly writable directories is security-sensitive -> replaced by SES1308 + +# Major security hotspots +dotnet_diagnostic.S1313.severity = none # Using hardcoded IP addresses is security-sensitive -> off: hardcoded-IP heuristic; too noisy to enforce +dotnet_diagnostic.S2077.severity = none # Formatting SQL queries is security-sensitive -> replaced by CA2100 +dotnet_diagnostic.S5693.severity = none # Allowing requests with excessive content length is security-sensitive -> replaced by SES1505 +dotnet_diagnostic.S5753.severity = none # Disabling ASP.NET "Request Validation" feature is security-sensitive -> replaced by obsolete (legacy ASP.NET request validation) +dotnet_diagnostic.S5766.severity = none # Creating Serializable objects without data validation checks is security-sensitive -> off: legacy BinaryFormatter deserialization; obsolete path, not used here +dotnet_diagnostic.S6444.severity = none # Not specifying a timeout for regular expressions is security-sensitive -> replaced by SES1509 +dotnet_diagnostic.S6640.severity = none # Using unsafe code blocks is security-sensitive -> off: unsafe-code audit; not enforced here + +# Minor security hotspots +dotnet_diagnostic.S2092.severity = none # Creating cookies without the "secure" flag is security-sensitive -> replaced by CA5382 +dotnet_diagnostic.S3330.severity = none # Creating cookies without the "HttpOnly" flag is security-sensitive -> replaced by CA5383 +dotnet_diagnostic.S4507.severity = none # Delivering code in production with debug features activated is security-sensitive -> off: debug features in production; partly covered by SES1506, rest not enforced +dotnet_diagnostic.S5122.severity = none # Having a permissive Cross-Origin Resource Sharing policy is security-sensitive -> replaced by SES1501 ############################################# -# NUnit Analyzers — enable all as errors +# JetBrains ReSharper / Rider Inspections ############################################# +# Mirrors the CA / SA / RCS choices above for Rider and ReSharper users. Only +# inspections with a clear mapping to our Roslyn analyzer rules are listed; +# every other ReSharper inspection keeps its default severity. The goal is to +# avoid Rider double-reporting things the Roslyn analyzers already catch and +# to keep IDE severities in sync with CI-enforced severities. + +################### +# ReSharper - Layout / Formatting (covered by StyleCop SA) +################### +resharper_redundant_linebreak_highlighting = none # covered by SA layout rules +resharper_missing_linebreak_highlighting = none # covered by SA layout rules +resharper_bad_empty_braces_line_breaks_highlighting = none # covered by SA1500 +resharper_missing_indent_highlighting = none # covered by editorconfig indent_size +resharper_missing_blank_lines_highlighting = none # covered by SA1516 / SA1517 +resharper_wrong_indent_size_highlighting = none # covered by editorconfig indent_size +resharper_bad_indent_highlighting = none # covered by editorconfig indent_size +resharper_bad_expression_braces_line_breaks_highlighting = none # covered by SA layout rules +resharper_multiple_spaces_highlighting = none # covered by SA1025 +resharper_bad_expression_braces_indent_highlighting = none # covered by SA layout rules +resharper_bad_control_braces_indent_highlighting = none # covered by SA layout rules +resharper_bad_preprocessor_indent_highlighting = none # covered by SA layout rules +resharper_redundant_blank_lines_highlighting = none # covered by SA1516 / SA1507 +resharper_multiple_statements_on_one_line_highlighting = none # covered by SA1119 / SA1503 +resharper_bad_braces_spaces_highlighting = none # covered by SA1012 / SA1013 +resharper_outdent_is_off_prev_level_highlighting = none # editorconfig indentation +resharper_bad_symbol_spaces_highlighting = none # covered by SA spacing rules +resharper_bad_colon_spaces_highlighting = none # covered by SA1024 +resharper_bad_semicolon_spaces_highlighting = none # covered by SA1002 +resharper_bad_square_brackets_spaces_highlighting = none # covered by SA1010 / SA1011 +resharper_bad_parens_spaces_highlighting = none # covered by SA1008 / SA1009 + +################### +# ReSharper - 'this' qualifier (matches SA1101 = none) +################### +resharper_redundant_this_qualifier_highlighting = none # we don't follow the this. prefix style +resharper_arrange_this_qualifier_highlighting = none # we don't follow the this. prefix style +resharper_arrange_default_access_modifier_highlighting = none # SA1400 requires explicit access modifiers — don't suggest removing them +resharper_redundant_default_access_modifier_highlighting = none # SA1400 requires explicit access modifiers — don't suggest removing them +resharper_redundant_access_modifier_highlighting = none # access modifiers are deliberate and SA1400 requires them +resharper_arrange_type_member_modifiers_highlighting = none # SA1206 handles modifier ordering; don't duplicate +resharper_redundant_empty_switch_section_highlighting = none # handled by RCS1070 + +################### +# ReSharper - Redundancies / cleanup +################### +resharper_redundant_using_directive_highlighting = error # IDE0005 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_qualifier_highlighting = error # IDE0001 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_name_qualifier_highlighting = error # IDE0001 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_base_qualifier_highlighting = none # IDE0009 is disabled (SA1101 = none) — don't nag in Rider either +resharper_redundant_cast_highlighting = none # handled by RCS1151 +resharper_redundant_explicit_array_creation_highlighting = none # handled by RCS1014 +resharper_redundant_empty_object_creation_argument_list_highlighting = none # handled by RCS1039 +resharper_redundant_lambda_signature_parentheses_highlighting = error # IDE0063 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_default_member_initializer_highlighting = none # handled by RCS1188 +resharper_redundant_field_initializer_highlighting = none # handled by RCS1129 +resharper_redundant_overriding_member_highlighting = none # handled by RCS1132 +resharper_redundant_verbatim_string_prefix_highlighting = none # handled by RCS1192 +resharper_redundant_string_interpolation_highlighting = none # handled by RCS1105 / RCS1214 +resharper_redundant_to_string_call_highlighting = none # handled by RCS1097 + +################### +# ReSharper - Naming (matches SA/CA naming rules) +################### +resharper_inconsistent_naming_highlighting = none # too many false positives on test/fixture naming; SA1300 family covers the real cases -# Structure Rules (NUnit1001 - ) -dotnet_diagnostic.NUnit1001.severity = error # TestCase args must match parameter types -dotnet_diagnostic.NUnit1002.severity = error # TestCaseSource should use nameof -dotnet_diagnostic.NUnit1003.severity = error # TestCase provided too few arguments -dotnet_diagnostic.NUnit1004.severity = error # TestCase provided too many arguments -dotnet_diagnostic.NUnit1005.severity = error # ExpectedResult type must match return type -dotnet_diagnostic.NUnit1006.severity = error # ExpectedResult must not be used on void methods -dotnet_diagnostic.NUnit1007.severity = error # Non-void method but no ExpectedResult provided -dotnet_diagnostic.NUnit1008.severity = error # ParallelScope.Self at assembly level has no effect -dotnet_diagnostic.NUnit1009.severity = error # ParallelScope.Children on non-parameterized test -dotnet_diagnostic.NUnit1010.severity = error # ParallelScope.Fixtures on a test method -dotnet_diagnostic.NUnit1011.severity = error # TestCaseSource member does not exist -dotnet_diagnostic.NUnit1012.severity = error # async test method must have non-void return type -dotnet_diagnostic.NUnit1013.severity = error # async method must use non-generic Task when no result -dotnet_diagnostic.NUnit1014.severity = error # async method must use Task when result expected -dotnet_diagnostic.NUnit1015.severity = error # Source type does not implement I(Async)Enumerable -dotnet_diagnostic.NUnit1016.severity = error # Source type lacks default constructor -dotnet_diagnostic.NUnit1017.severity = error # Specified source is not static -dotnet_diagnostic.NUnit1018.severity = error # TestCaseSource param count mismatch (target method) -dotnet_diagnostic.NUnit1019.severity = error # Source does not return I(Async)Enumerable -dotnet_diagnostic.NUnit1020.severity = error # Parameters provided to field/property source -dotnet_diagnostic.NUnit1021.severity = error # ValueSource should use nameof -dotnet_diagnostic.NUnit1022.severity = error # Specified ValueSource is not static -dotnet_diagnostic.NUnit1023.severity = error # ValueSource cannot supply required parameters -dotnet_diagnostic.NUnit1024.severity = error # ValueSource does not return I(Async)Enumerable -dotnet_diagnostic.NUnit1025.severity = error # ValueSource member does not exist -dotnet_diagnostic.NUnit1026.severity = error # Test or setup/teardown method is not public -dotnet_diagnostic.NUnit1027.severity = error # Test method has parameters but no arguments supplied -dotnet_diagnostic.NUnit1028.severity = error # Non-test method is public -dotnet_diagnostic.NUnit1029.severity = error # TestCaseSource param count mismatch (Test method) -dotnet_diagnostic.NUnit1030.severity = error # TestCaseSource parameter type mismatch (Test method) -dotnet_diagnostic.NUnit1031.severity = error # ValuesAttribute args must match parameter types -dotnet_diagnostic.NUnit1032.severity = error # IDisposable field/property should be disposed in TearDown -dotnet_diagnostic.NUnit1033.severity = error # TestContext.Write methods will be obsolete -dotnet_diagnostic.NUnit1034.severity = error # Base TestFixtures should be abstract -dotnet_diagnostic.NUnit1035.severity = error # Range 'step' parameter cannot be zero -dotnet_diagnostic.NUnit1036.severity = error # Range: from < to when step is positive -dotnet_diagnostic.NUnit1037.severity = error # Range: from > to when step is negative -dotnet_diagnostic.NUnit1038.severity = error # Attribute values' types must match parameter type - -# Assertion Rules (NUnit2001 - ) -dotnet_diagnostic.NUnit2001.severity = error # Prefer Assert.That(..., Is.False) over ClassicAssert.False -dotnet_diagnostic.NUnit2002.severity = error # Prefer Assert.That(..., Is.False) over ClassicAssert.IsFalse -dotnet_diagnostic.NUnit2003.severity = error # Prefer Assert.That(..., Is.True) over ClassicAssert.IsTrue -dotnet_diagnostic.NUnit2004.severity = error # Prefer Assert.That(..., Is.True) over ClassicAssert.True -dotnet_diagnostic.NUnit2005.severity = error # Prefer Is.EqualTo over AreEqual -dotnet_diagnostic.NUnit2006.severity = error # Prefer Is.Not.EqualTo over AreNotEqual -dotnet_diagnostic.NUnit2007.severity = error # Actual value should not be a constant -dotnet_diagnostic.NUnit2008.severity = error # Incorrect IgnoreCase usage -dotnet_diagnostic.NUnit2009.severity = error # Same value used for actual and expected -dotnet_diagnostic.NUnit2010.severity = error # Use EqualConstraint for better messages -dotnet_diagnostic.NUnit2011.severity = error # Use ContainsConstraint for better messages -dotnet_diagnostic.NUnit2012.severity = error # Use StartsWithConstraint for better messages -dotnet_diagnostic.NUnit2013.severity = error # Use EndsWithConstraint for better messages -dotnet_diagnostic.NUnit2014.severity = error # Use SomeItemsConstraint for better messages -dotnet_diagnostic.NUnit2015.severity = error # Prefer Is.SameAs over AreSame -dotnet_diagnostic.NUnit2016.severity = error # Prefer Is.Null over ClassicAssert.Null -dotnet_diagnostic.NUnit2017.severity = error # Prefer Is.Null over ClassicAssert.IsNull -dotnet_diagnostic.NUnit2018.severity = error # Prefer Is.Not.Null over ClassicAssert.NotNull -dotnet_diagnostic.NUnit2019.severity = error # Prefer Is.Not.Null over ClassicAssert.IsNotNull -dotnet_diagnostic.NUnit2020.severity = error # Incompatible types for SameAs constraint -dotnet_diagnostic.NUnit2021.severity = error # Incompatible types for EqualTo constraint -dotnet_diagnostic.NUnit2022.severity = error # Missing property required for constraint -dotnet_diagnostic.NUnit2023.severity = error # Invalid NullConstraint usage -dotnet_diagnostic.NUnit2024.severity = error # Wrong actual type with String constraint -dotnet_diagnostic.NUnit2025.severity = error # Wrong actual type with ContainsConstraint -dotnet_diagnostic.NUnit2026.severity = error # Wrong actual type with SomeItems+EqualConstraint -dotnet_diagnostic.NUnit2027.severity = error # Prefer Is.GreaterThan over ClassicAssert.Greater -dotnet_diagnostic.NUnit2028.severity = error # Prefer Is.GreaterThanOrEqualTo over GreaterOrEqual -dotnet_diagnostic.NUnit2029.severity = error # Prefer Is.LessThan over ClassicAssert.Less -dotnet_diagnostic.NUnit2030.severity = error # Prefer Is.LessThanOrEqualTo over LessOrEqual -dotnet_diagnostic.NUnit2031.severity = error # Prefer Is.Not.SameAs over AreNotSame -dotnet_diagnostic.NUnit2032.severity = error # Prefer Is.Zero over ClassicAssert.Zero -dotnet_diagnostic.NUnit2033.severity = error # Prefer Is.Not.Zero over ClassicAssert.NotZero -dotnet_diagnostic.NUnit2034.severity = error # Prefer Is.NaN over ClassicAssert.IsNaN -dotnet_diagnostic.NUnit2035.severity = error # Prefer Is.Empty over ClassicAssert.IsEmpty -dotnet_diagnostic.NUnit2036.severity = error # Prefer Is.Not.Empty over ClassicAssert.IsNotEmpty -dotnet_diagnostic.NUnit2037.severity = error # Prefer Does.Contain over ClassicAssert.Contains -dotnet_diagnostic.NUnit2038.severity = error # Prefer Is.InstanceOf over ClassicAssert.IsInstanceOf -dotnet_diagnostic.NUnit2039.severity = error # Prefer Is.Not.InstanceOf over ClassicAssert.IsNotInstanceOf -dotnet_diagnostic.NUnit2040.severity = error # Non-reference types for SameAs constraint -dotnet_diagnostic.NUnit2041.severity = error # Incompatible types for comparison constraint -dotnet_diagnostic.NUnit2042.severity = error # Comparison constraint on object -dotnet_diagnostic.NUnit2043.severity = error # Use ComparisonConstraint for better messages -dotnet_diagnostic.NUnit2044.severity = error # Non-delegate actual parameter -dotnet_diagnostic.NUnit2045.severity = error # Use Assert.EnterMultipleScope or Assert.Multiple -dotnet_diagnostic.NUnit2046.severity = error # Use CollectionConstraint for better messages -dotnet_diagnostic.NUnit2047.severity = error # Incompatible types for Within constraint -dotnet_diagnostic.NUnit2048.severity = error # Prefer Assert.That over StringAssert -dotnet_diagnostic.NUnit2049.severity = error # Prefer Assert.That over CollectionAssert -dotnet_diagnostic.NUnit2050.severity = error # NUnit 4 no longer supports string.Format spec -dotnet_diagnostic.NUnit2051.severity = error # Prefer Is.Positive over ClassicAssert.Positive -dotnet_diagnostic.NUnit2052.severity = error # Prefer Is.Negative over ClassicAssert.Negative -dotnet_diagnostic.NUnit2053.severity = error # Prefer Is.AssignableFrom over ClassicAssert.IsAssignableFrom -dotnet_diagnostic.NUnit2054.severity = error # Prefer Is.Not.AssignableFrom over ClassicAssert.IsNotAssignableFrom -dotnet_diagnostic.NUnit2055.severity = error # Prefer Is.InstanceOf over 'is T' expression -dotnet_diagnostic.NUnit2056.severity = error # Prefer Assert.EnterMultipleScope statement over Multiple - -# Suppressor Rules (NUnit3001 - ) -dotnet_diagnostic.NUnit3001.severity = error # Expression checked in NotNull/IsNotNull/Assert.That -dotnet_diagnostic.NUnit3002.severity = error # Field/Property initialized in SetUp/OneTimeSetUp -dotnet_diagnostic.NUnit3003.severity = error # TestFixture instantiated via reflection -dotnet_diagnostic.NUnit3004.severity = error # Field should be disposed in TearDown/OneTimeTearDown - -# Style Rules (NUnit4001 - ) -dotnet_diagnostic.NUnit4001.severity = error # Simplify the Values attribute -dotnet_diagnostic.NUnit4002.severity = error # Use Specific constraint +################### +# ReSharper - Unused code (matches CA1801 / CA1823 / IDE0051-0052) +################### +resharper_unused_parameter_local_highlighting = none # handled by CA1801 (private/internal surface) +resharper_unused_parameter_global_highlighting = none # public-API parameters can be unused for compat +resharper_unused_member_local_highlighting = error # IDE0051 may not fire at compile time reliably — keep Rider nagging +resharper_unused_member_global_highlighting = suggestion # public-API members may be intentionally exposed +resharper_unused_field_local_highlighting = none # handled by CA1823 +resharper_unused_field_global_highlighting = none # public-API fields may be intentionally exposed +resharper_unused_type_local_highlighting = error # IDE0052 may not fire at compile time reliably — keep Rider nagging +resharper_unused_type_global_highlighting = suggestion # public-API types may be intentionally exposed +resharper_unused_variable_highlighting = none # handled by CS0219 +resharper_unused_auto_property_accessor_local_highlighting = none # data-class setters are intentional even when unused locally +resharper_unused_auto_property_accessor_global_highlighting = none # public auto-properties may be intentionally exposed +################### +# ReSharper - Code quality (matches CA/RCS semantic analyzers) +################### +resharper_class_never_instantiated_local_highlighting = none # handled by CA1812 +resharper_class_never_instantiated_global_highlighting = suggestion # public types may be instantiated externally +resharper_class_cannot_be_instantiated_highlighting = none # handled by CA1052 +resharper_class_can_be_sealed_local_highlighting = none # handled by CA1852 (private/internal surface) +resharper_class_can_be_sealed_global_highlighting = none # public types stay open for inheritance +resharper_use_nameof_expression_highlighting = none # handled by CA1507 / RCS1015 +resharper_possible_null_reference_exception_highlighting = none # handled bynullability analyzer +resharper_possible_invalid_operation_exception_highlighting = none # handled bynullability analyzer +resharper_possible_multiple_enumeration_highlighting = none # handled by CA1851 +resharper_possible_mistaken_argument_null_check_highlighting = none # handled by CA1062 +resharper_convert_to_constant_local_highlighting = none # handled by RCS1118 +resharper_convert_to_static_class_highlighting = none # handled by CA1052 +resharper_convert_to_auto_property_highlighting = none # handled by RCS1085 +resharper_convert_to_auto_property_when_possible_highlighting = none # handled by RCS1085 +resharper_convert_to_auto_property_with_private_setter_highlighting = none # handled by RCS1085 +resharper_auto_property_can_be_made_get_only_local_highlighting = none # handled by RCS1170 +resharper_auto_property_can_be_made_get_only_global_highlighting = suggestion # public setters kept for API consumers +resharper_member_can_be_made_static_local_highlighting = none # handled by CA1822 (private/internal surface) +resharper_member_can_be_made_static_global_highlighting = none # public members kept virtual for inheritance +resharper_member_can_be_private_local_highlighting = none # access modifiers are deliberate — don't suggest tightening +resharper_method_has_async_overload_highlighting = none # sync call sites are intentional (e.g. test setup, disposal, sample code) +resharper_member_can_be_private_global_highlighting = none # access modifiers are deliberate — don't suggest tightening +resharper_field_can_be_made_read_only_local_highlighting = none # handled by RCS1169 +resharper_field_can_be_made_read_only_global_highlighting = none # handled by RCS1169 +resharper_merge_into_pattern_highlighting = none # handled by RCS1220 +resharper_merge_conditional_expression_highlighting = none # handled by RCS1173 +resharper_merge_sequential_checks_highlighting = none # handled by RCS1206 +resharper_simplify_conditional_ternary_expression_highlighting = none # handled by RCS1049 / RCS1104 +resharper_simplify_linq_expression_highlighting = error # handled by RCS1077 +resharper_string_compare_to_is_culture_specific_highlighting = none # handled by CA1310 +resharper_string_ends_with_is_culture_specific_highlighting = none # handled by CA1310 +resharper_string_index_of_is_culture_specific_1_highlighting = none # handled by CA1310 +resharper_string_index_of_is_culture_specific_2_highlighting = none # handled by CA1310 +resharper_string_index_of_is_culture_specific_3_highlighting = none # handled by CA1310 +resharper_string_last_index_of_is_culture_specific_1_highlighting = none # handled by CA1310 +resharper_string_last_index_of_is_culture_specific_2_highlighting = none # handled by CA1310 +resharper_string_last_index_of_is_culture_specific_3_highlighting = none # handled by CA1310 +resharper_string_starts_with_is_culture_specific_highlighting = none # handled by CA1310 +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting = none # handled by CA1304 +################### +# ReSharper - Control flow / nesting +################### +resharper_invert_if_highlighting = suggestion # related to RCS1208 — Rider is more aggressive +resharper_tail_recursive_call_highlighting = suggestion + +################### +# ReSharper - Documentation (covered by StyleCop SA1600 family) +################### +resharper_missing_xml_doc_highlighting = none # covered by SA1600 family +resharper_missing_xml_doc_global_highlighting = none # covered by SA1600 family +resharper_invalid_xml_doc_comment_highlighting = none # handled by SA1612 / SA1613 + +################### +# ReSharper - Typo / spelling (not enforced) +################### +resharper_comment_typo_highlighting = none # typo checks are out of scope +resharper_string_literal_typo_highlighting = none # typo checks are out of scope +resharper_identifier_typo_highlighting = none # typo checks are out of scope +resharper_markup_text_typo_highlighting = none # typo checks are out of scope + +############################################# +# Other Settings +############################################# +vsspell_dictionary_languages = en-US + +############################################# # C++ Files +############################################# [*.{cpp,h,in}] curly_bracket_next_line = true indent_brace_style = Allman -# Xml project files +############################################# +# XML Files +############################################# [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] indent_size = 2 -# Xml build files [*.builds] indent_size = 2 -# Xml files [*.{xml,stylecop,resx,ruleset}] indent_size = 2 -# Xml config files [*.{props,targets,config,nuspec}] indent_size = 2 -# Shell scripts +############################################# +# Shell Scripts +############################################# [*.sh] end_of_line = lf + [*.{cmd, bat}] end_of_line = crlf + +############################################# +# Test projects (TUnit) +############################################# +# TUnit instantiates test classes per test, so they must remain instance classes and +# cannot be marked static — even when a partial declaration happens to hold only static +# members (the instance [Test] methods live in sibling partial files). diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a510aa72 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.cs text eol=lf diff --git a/README.md b/README.md index 22926bcd..4959da47 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ dotnet add package ReactiveUI.SourceGenerators Ensure the package is loaded with `PrivateAssets="all"` to avoid issues with generated code in consuming projects. +ReactiveUI V24.x.x consumers can reference either `ReactiveUI` for the primitives-based API without +System.Reactive, or `ReactiveUI.Reactive` for the System.Reactive-based API. The generators detect +the referenced API surface automatically. ReactiveUI releases from 23.2.28 remain supported. + ## Overview ReactiveUI Source Generators automatically generate ReactiveUI objects to streamline your code. These Source Generators are designed to work with ReactiveUI V23.2.28+ and support the following features: diff --git a/src/Directory.Build.props b/src/Directory.Build.props index dffd0be2..edcb1553 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -94,11 +94,10 @@ - + + + - - - diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index bba73e00..c6ec6aeb 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,13 +1,16 @@ - + true true 4.14.0 + 3.40.0 - + + + @@ -25,12 +28,14 @@ + - - + + + diff --git a/src/ReactiveUI.SourceGenerator.Tests/ModuleInitializer.cs b/src/ReactiveUI.SourceGenerator.Tests/ModuleInitializer.cs index a6fb5d26..245f0215 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/ModuleInitializer.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/ModuleInitializer.cs @@ -1,16 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -/// -/// Initializes the source generator verifiers. -/// +namespace ReactiveUI.SourceGenerator.Tests; + +/// Initializes the source generator verifiers. public static class ModuleInitializer { - /// - /// Initializes the source generators. - /// + /// Initializes the source generators. [ModuleInitializer] public static void Init() => VerifySourceGenerators.Initialize(); } diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.CalledValue#TestNs.TestVM.Properties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.CalledValue#TestNs.TestVM.Properties.g.verified.cs index 9a88cf2b..3cfe3ae9 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.CalledValue#TestNs.TestVM.Properties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.CalledValue#TestNs.TestVM.Properties.g.verified.cs @@ -13,7 +13,7 @@ public partial class TestVM /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public string Value - { + { get => value; [global::System.Diagnostics.CodeAnalysis.MemberNotNull("this.value")] set diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.WithInit#TestNs.TestVM.Properties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.WithInit#TestNs.TestVM.Properties.g.verified.cs index 5da35b54..2f60d617 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.WithInit#TestNs.TestVM.Properties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.WithInit#TestNs.TestVM.Properties.g.verified.cs @@ -13,7 +13,7 @@ public partial class TestVM /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public required string MustBeSet - { + { get => _mustBeSet; [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_mustBeSet")] init diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs index 77c1462b..1d9a3fe9 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _test1Command; + private global::ReactiveUI.ReactiveCommand? _test1Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - internal global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } + internal global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.AsyncWithParam#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.AsyncWithParam#TestNs.TestVM.ReactiveCommands.g.verified.cs index c22cc65e..0d1250a0 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.AsyncWithParam#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.AsyncWithParam#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _test3Command; + private global::ReactiveUI.ReactiveCommand? _test3Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand Test3Command { get => _test3Command ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(Test3); } + public global::ReactiveUI.ReactiveCommand Test3Command { get => _test3Command ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(Test3); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Basic#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Basic#TestNs.TestVM.ReactiveCommands.g.verified.cs index 4e15fb71..76df3c5a 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Basic#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Basic#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _test1Command; + private global::ReactiveUI.ReactiveCommand? _test1Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } + public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#TestNs.TestVM.ReactiveCommands.g.verified.cs index b8b49fb6..805de3be 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _test1Command; + private global::ReactiveUI.ReactiveCommand? _test1Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(Test1); } + public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(Test1); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass1.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass1.ReactiveCommands.g.verified.cs index 3e52387c..6eed3de1 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass1.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass1.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestViewModel3+TestInnerClass1.ReactiveCommands.g.cs +//HintName: TestNs.TestViewModel3+TestInnerClass1.ReactiveCommands.g.cs // #pragma warning disable @@ -11,11 +11,11 @@ public partial class TestViewModel3 public partial class TestInnerClass1 { - private global::ReactiveUI.ReactiveCommand? _testI1Command; + private global::ReactiveUI.ReactiveCommand? _testI1Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand TestI1Command { get => _testI1Command ??= global::ReactiveUI.ReactiveCommand.Create(TestI1); } + public global::ReactiveUI.ReactiveCommand TestI1Command { get => _testI1Command ??= global::ReactiveUI.ReactiveCommand.Create(TestI1); } } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2+TestInnerClass3.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2+TestInnerClass3.ReactiveCommands.g.verified.cs index c3bf082f..59f9102a 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2+TestInnerClass3.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2+TestInnerClass3.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestViewModel3+TestInnerClass2+TestInnerClass3.ReactiveCommands.g.cs +//HintName: TestNs.TestViewModel3+TestInnerClass2+TestInnerClass3.ReactiveCommands.g.cs // #pragma warning disable @@ -13,11 +13,11 @@ public partial class TestInnerClass2 public partial class TestInnerClass3 { - private global::ReactiveUI.ReactiveCommand? _testI3Command; + private global::ReactiveUI.ReactiveCommand? _testI3Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand TestI3Command { get => _testI3Command ??= global::ReactiveUI.ReactiveCommand.Create(TestI3); } + public global::ReactiveUI.ReactiveCommand TestI3Command { get => _testI3Command ??= global::ReactiveUI.ReactiveCommand.Create(TestI3); } } } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2.ReactiveCommands.g.verified.cs index 2613e4fc..01895dd3 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3+TestInnerClass2.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestViewModel3+TestInnerClass2.ReactiveCommands.g.cs +//HintName: TestNs.TestViewModel3+TestInnerClass2.ReactiveCommands.g.cs // #pragma warning disable @@ -11,11 +11,11 @@ public partial class TestViewModel3 public partial class TestInnerClass2 { - private global::ReactiveUI.ReactiveCommand? _testI2Command; + private global::ReactiveUI.ReactiveCommand? _testI2Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand TestI2Command { get => _testI2Command ??= global::ReactiveUI.ReactiveCommand.Create(TestI2); } + public global::ReactiveUI.ReactiveCommand TestI2Command { get => _testI2Command ??= global::ReactiveUI.ReactiveCommand.Create(TestI2); } } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3.ReactiveCommands.g.verified.cs index a58e05ae..a05aefdb 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Nested#TestNs.TestViewModel3.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestViewModel3.ReactiveCommands.g.cs +//HintName: TestNs.TestViewModel3.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestViewModel3 { - private global::ReactiveUI.ReactiveCommand? _test1Command; + private global::ReactiveUI.ReactiveCommand? _test1Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } + public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Scheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Scheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs index 4e15fb71..76df3c5a 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Scheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Scheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _test1Command; + private global::ReactiveUI.ReactiveCommand? _test1Command; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } + public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs index 0c7f7dbd..c8c0ea6a 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,21 +9,21 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _createCommand; + private global::ReactiveUI.ReactiveCommand? _createCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand CreateCommand { get => _createCommand ??= global::ReactiveUI.ReactiveCommand.Create(Create); } - private global::ReactiveUI.ReactiveCommand? _saveCommand; + public global::ReactiveUI.ReactiveCommand CreateCommand { get => _createCommand ??= global::ReactiveUI.ReactiveCommand.Create(Create); } + private global::ReactiveUI.ReactiveCommand? _saveCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand SaveCommand { get => _saveCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(Save, CanExecuteSave); } - private global::ReactiveUI.ReactiveCommand? _deleteCommand; + public global::ReactiveUI.ReactiveCommand SaveCommand { get => _saveCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(Save, CanExecuteSave); } + private global::ReactiveUI.ReactiveCommand? _deleteCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand DeleteCommand { get => _deleteCommand ??= global::ReactiveUI.ReactiveCommand.Create(Delete, CanExecuteDelete); } + public global::ReactiveUI.ReactiveCommand DeleteCommand { get => _deleteCommand ??= global::ReactiveUI.ReactiveCommand.Create(Delete, CanExecuteDelete); } private global::ReactiveUI.ReactiveCommand? _calculateCommand; diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle+Inner.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle+Inner.ReactiveCommands.g.verified.cs index 5738deb3..e64eaf9b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle+Inner.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle+Inner.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.Outer+Middle+Inner.ReactiveCommands.g.cs +//HintName: TestNs.Outer+Middle+Inner.ReactiveCommands.g.cs // #pragma warning disable @@ -13,11 +13,11 @@ public partial class Middle public partial class Inner { - private global::ReactiveUI.ReactiveCommand? _innerCommandCommand; + private global::ReactiveUI.ReactiveCommand? _innerCommandCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand InnerCommandCommand { get => _innerCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(InnerCommand); } + public global::ReactiveUI.ReactiveCommand InnerCommandCommand { get => _innerCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(InnerCommand); } } } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle.ReactiveCommands.g.verified.cs index cbc3e4b2..81731555 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer+Middle.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.Outer+Middle.ReactiveCommands.g.cs +//HintName: TestNs.Outer+Middle.ReactiveCommands.g.cs // #pragma warning disable @@ -11,11 +11,11 @@ public partial class Outer public partial class Middle { - private global::ReactiveUI.ReactiveCommand? _middleCommandCommand; + private global::ReactiveUI.ReactiveCommand? _middleCommandCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand MiddleCommandCommand { get => _middleCommandCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(MiddleCommand); } + public global::ReactiveUI.ReactiveCommand MiddleCommandCommand { get => _middleCommandCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(MiddleCommand); } } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer.ReactiveCommands.g.verified.cs index ffa7f597..3b0b8370 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInDeeplyNestedClass#TestNs.Outer.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.Outer.ReactiveCommands.g.cs +//HintName: TestNs.Outer.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class Outer { - private global::ReactiveUI.ReactiveCommand? _outerCommandCommand; + private global::ReactiveUI.ReactiveCommand? _outerCommandCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand OuterCommandCommand { get => _outerCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(OuterCommand); } + public global::ReactiveUI.ReactiveCommand OuterCommandCommand { get => _outerCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(OuterCommand); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInRecordClass#TestNs.TestVMRecord.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInRecordClass#TestNs.TestVMRecord.ReactiveCommands.g.verified.cs index 67127eae..906549b5 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInRecordClass#TestNs.TestVMRecord.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInRecordClass#TestNs.TestVMRecord.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVMRecord.ReactiveCommands.g.cs +//HintName: TestNs.TestVMRecord.ReactiveCommands.g.cs // #pragma warning disable @@ -9,16 +9,16 @@ namespace TestNs public partial record TestVMRecord { - private global::ReactiveUI.ReactiveCommand? _doSomethingCommand; + private global::ReactiveUI.ReactiveCommand? _doSomethingCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand DoSomethingCommand { get => _doSomethingCommand ??= global::ReactiveUI.ReactiveCommand.Create(DoSomething); } - private global::ReactiveUI.ReactiveCommand? _getValueCommand; + public global::ReactiveUI.ReactiveCommand DoSomethingCommand { get => _doSomethingCommand ??= global::ReactiveUI.ReactiveCommand.Create(DoSomething); } + private global::ReactiveUI.ReactiveCommand? _getValueCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand GetValueCommand { get => _getValueCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetValueAsync); } + public global::ReactiveUI.ReactiveCommand GetValueCommand { get => _getValueCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetValueAsync); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecute#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecute#TestNs.TestVM.ReactiveCommands.g.verified.cs index 55c98372..a08b7075 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecute#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecute#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _doWorkCommand; + private global::ReactiveUI.ReactiveCommand? _doWorkCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand DoWorkCommand { get => _doWorkCommand ??= global::ReactiveUI.ReactiveCommand.Create(DoWork, CanDoWork); } + public global::ReactiveUI.ReactiveCommand DoWorkCommand { get => _doWorkCommand ??= global::ReactiveUI.ReactiveCommand.Create(DoWork, CanDoWork); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteAndScheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteAndScheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs index daae8683..948e72ac 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteAndScheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteAndScheduler#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _executeWithSchedulerCommand; + private global::ReactiveUI.ReactiveCommand? _executeWithSchedulerCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand ExecuteWithSchedulerCommand { get => _executeWithSchedulerCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(ExecuteWithScheduler, CanExecuteCommand); } + public global::ReactiveUI.ReactiveCommand ExecuteWithSchedulerCommand { get => _executeWithSchedulerCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(ExecuteWithScheduler, CanExecuteCommand); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#TestNs.TestVM.ReactiveCommands.g.verified.cs index be917d9f..b764dc1c 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _runCommand; + private global::ReactiveUI.ReactiveCommand? _runCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand RunCommand { get => _runCommand ??= global::ReactiveUI.ReactiveCommand.Create(Run, CanRun()); } + public global::ReactiveUI.ReactiveCommand RunCommand { get => _runCommand ??= global::ReactiveUI.ReactiveCommand.Create(Run, CanRun()); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCancellationToken#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCancellationToken#TestNs.TestVM.ReactiveCommands.g.verified.cs index da7cc0c7..93779cd4 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCancellationToken#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCancellationToken#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _longRunningOperationCommand; + private global::ReactiveUI.ReactiveCommand? _longRunningOperationCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand LongRunningOperationCommand { get => _longRunningOperationCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(LongRunningOperation); } + public global::ReactiveUI.ReactiveCommand LongRunningOperationCommand { get => _longRunningOperationCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(LongRunningOperation); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithComplexGenericReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithComplexGenericReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs index 9c419215..b914bdac 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithComplexGenericReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithComplexGenericReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,16 +9,16 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand>>? _getComplexDataCommand; + private global::ReactiveUI.ReactiveCommand>>? _getComplexDataCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand>> GetComplexDataCommand { get => _getComplexDataCommand ??= global::ReactiveUI.ReactiveCommand.Create(GetComplexData); } - private global::ReactiveUI.ReactiveCommand>? _getAsyncComplexDataCommand; + public global::ReactiveUI.ReactiveCommand>> GetComplexDataCommand { get => _getComplexDataCommand ??= global::ReactiveUI.ReactiveCommand.Create(GetComplexData); } + private global::ReactiveUI.ReactiveCommand>? _getAsyncComplexDataCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand> GetAsyncComplexDataCommand { get => _getAsyncComplexDataCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetAsyncComplexData); } + public global::ReactiveUI.ReactiveCommand> GetAsyncComplexDataCommand { get => _getAsyncComplexDataCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetAsyncComplexData); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithDelegateParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithDelegateParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs index b5f3da27..dd6df93d 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithDelegateParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithDelegateParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _executeActionCommand; + private global::ReactiveUI.ReactiveCommand? _executeActionCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand ExecuteActionCommand { get => _executeActionCommand ??= global::ReactiveUI.ReactiveCommand.Create(ExecuteAction); } + public global::ReactiveUI.ReactiveCommand ExecuteActionCommand { get => _executeActionCommand ??= global::ReactiveUI.ReactiveCommand.Create(ExecuteAction); } private global::ReactiveUI.ReactiveCommand?, int>? _executeFuncCommand; diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithEnumParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithEnumParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs index 23b4bcad..12abdb57 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithEnumParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithEnumParameter#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -14,11 +14,11 @@ public partial class TestVM [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public global::ReactiveUI.ReactiveCommand GetStatusMessageCommand { get => _getStatusMessageCommand ??= global::ReactiveUI.ReactiveCommand.Create(GetStatusMessage); } - private global::ReactiveUI.ReactiveCommand? _setStatusCommand; + private global::ReactiveUI.ReactiveCommand? _setStatusCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand SetStatusCommand { get => _setStatusCommand ??= global::ReactiveUI.ReactiveCommand.Create(SetStatus); } + public global::ReactiveUI.ReactiveCommand SetStatusCommand { get => _setStatusCommand ??= global::ReactiveUI.ReactiveCommand.Create(SetStatus); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithObservableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithObservableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs index 2ffee6a6..b8cc49f3 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithObservableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithObservableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _getObservableSequenceCommand; + private global::ReactiveUI.ReactiveCommand? _getObservableSequenceCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand GetObservableSequenceCommand { get => _getObservableSequenceCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromObservable(GetObservableSequence); } + public global::ReactiveUI.ReactiveCommand GetObservableSequenceCommand { get => _getObservableSequenceCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromObservable(GetObservableSequence); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs index 1cefe47e..a58ae7e2 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _privateCommandCommand; + private global::ReactiveUI.ReactiveCommand? _privateCommandCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - private global::ReactiveUI.ReactiveCommand PrivateCommandCommand { get => _privateCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(PrivateCommand); } + private global::ReactiveUI.ReactiveCommand PrivateCommandCommand { get => _privateCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(PrivateCommand); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs index 14c54269..92c96a39 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,11 +9,11 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _protectedCommandCommand; + private global::ReactiveUI.ReactiveCommand? _protectedCommandCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - protected global::ReactiveUI.ReactiveCommand ProtectedCommandCommand { get => _protectedCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(ProtectedCommand); } + protected global::ReactiveUI.ReactiveCommand ProtectedCommandCommand { get => _protectedCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(ProtectedCommand); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTaskOfNullableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTaskOfNullableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs index d52497f1..63b877ed 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTaskOfNullableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTaskOfNullableReturn#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,16 +9,16 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _getNullableStringCommand; + private global::ReactiveUI.ReactiveCommand? _getNullableStringCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand GetNullableStringCommand { get => _getNullableStringCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetNullableStringAsync); } - private global::ReactiveUI.ReactiveCommand? _getNullableIntCommand; + public global::ReactiveUI.ReactiveCommand GetNullableStringCommand { get => _getNullableStringCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetNullableStringAsync); } + private global::ReactiveUI.ReactiveCommand? _getNullableIntCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand GetNullableIntCommand { get => _getNullableIntCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetNullableIntAsync); } + public global::ReactiveUI.ReactiveCommand GetNullableIntCommand { get => _getNullableIntCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetNullableIntAsync); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTupleReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTupleReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs index 63bf0d6d..dd6f90f7 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTupleReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithTupleReturnType#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,16 +9,16 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _getNamedTupleCommand; + private global::ReactiveUI.ReactiveCommand? _getNamedTupleCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand GetNamedTupleCommand { get => _getNamedTupleCommand ??= global::ReactiveUI.ReactiveCommand.Create(GetNamedTuple); } - private global::ReactiveUI.ReactiveCommand? _getAsyncTupleCommand; + public global::ReactiveUI.ReactiveCommand GetNamedTupleCommand { get => _getNamedTupleCommand ??= global::ReactiveUI.ReactiveCommand.Create(GetNamedTuple); } + private global::ReactiveUI.ReactiveCommand? _getAsyncTupleCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand GetAsyncTupleCommand { get => _getAsyncTupleCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetAsyncTuple); } + public global::ReactiveUI.ReactiveCommand GetAsyncTupleCommand { get => _getAsyncTupleCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(GetAsyncTuple); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithValueTask#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithValueTask#TestNs.TestVM.ReactiveCommands.g.verified.cs index bb3ea9d3..2d607288 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithValueTask#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithValueTask#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,16 +9,16 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _doValueTaskWorkCommand; + private global::ReactiveUI.ReactiveCommand? _doValueTaskWorkCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand DoValueTaskWorkCommand { get => _doValueTaskWorkCommand ??= global::ReactiveUI.ReactiveCommand.Create(DoValueTaskWork); } - private global::ReactiveUI.ReactiveCommand>? _getValueTaskResultCommand; + public global::ReactiveUI.ReactiveCommand DoValueTaskWorkCommand { get => _doValueTaskWorkCommand ??= global::ReactiveUI.ReactiveCommand.Create(DoValueTaskWork); } + private global::ReactiveUI.ReactiveCommand>? _getValueTaskResultCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand> GetValueTaskResultCommand { get => _getValueTaskResultCommand ??= global::ReactiveUI.ReactiveCommand.Create(GetValueTaskResult); } + public global::ReactiveUI.ReactiveCommand> GetValueTaskResultCommand { get => _getValueTaskResultCommand ??= global::ReactiveUI.ReactiveCommand.Create(GetValueTaskResult); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#TestNs.TestVM.ReactiveCommands.g.verified.cs index aa900e9c..88ee9a07 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#TestNs.TestVM.ReactiveCommands.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#TestNs.TestVM.ReactiveCommands.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: TestNs.TestVM.ReactiveCommands.g.cs +//HintName: TestNs.TestVM.ReactiveCommands.g.cs // #pragma warning disable @@ -9,16 +9,16 @@ namespace TestNs public partial class TestVM { - private global::ReactiveUI.ReactiveCommand? _createCommand; + private global::ReactiveUI.ReactiveCommand? _createCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand CreateCommand { get => _createCommand ??= global::ReactiveUI.ReactiveCommand.Create(Create); } - private global::ReactiveUI.ReactiveCommand? _loadCommand; + public global::ReactiveUI.ReactiveCommand CreateCommand { get => _createCommand ??= global::ReactiveUI.ReactiveCommand.Create(Create); } + private global::ReactiveUI.ReactiveCommand? _loadCommand; [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::ReactiveUI.ReactiveCommand LoadCommand { get => _loadCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(LoadAsync); } + public global::ReactiveUI.ReactiveCommand LoadCommand { get => _loadCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(LoadAsync); } } } #nullable restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj index 45f4f2bf..4b13e39b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj +++ b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj @@ -1,4 +1,4 @@ - + $(TestTfms) @@ -23,8 +23,11 @@ + + + @@ -61,7 +64,9 @@ - + + global,RoslynSourceGenerator + diff --git a/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs b/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs index dd7fc2ce..6037cba2 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs @@ -1,21 +1,16 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// A base class for handling test setup and teardown. -/// +/// A base class for handling test setup and teardown. /// Type of Incremental Generator. /// -public abstract class TestBase : IDisposable +public class TestBase : IDisposable where T : IIncrementalGenerator, new() { - /// - /// Gets the TestHelper instance. - /// + /// Gets the TestHelper instance. protected TestHelper TestHelper { get; } = new(); /// @@ -25,15 +20,15 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Disposes the resources used by the TestBase. - /// + /// Disposes the resources used by the TestBase. /// True if called from Dispose method, false if called from finalizer. protected virtual void Dispose(bool isDisposing) { - if (isDisposing) + if (!isDisposing) { - TestHelper.Dispose(); + return; } + + TestHelper.Dispose(); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs index abd8a9c4..1d4f49f2 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs @@ -1,10 +1,9 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using ReactiveUI.SourceGenerators.WinForms; namespace ReactiveUI.SourceGenerator.Tests; @@ -18,192 +17,93 @@ namespace ReactiveUI.SourceGenerator.Tests; public sealed partial class TestHelper : IDisposable where T : IIncrementalGenerator, new() { - // Cache support references per generator type T. The support assembly compiles attribute - // definitions that are NOT injected by T via RegisterPostInitializationOutput — an expensive - // Roslyn compilation + Emit step that produces an identical result for every test in the same - // generator class. Compute it once and reuse it for all subsequent tests. - private static readonly Lazy> supportReferences = - new(CreateSupportReferences, LazyThreadSafetyMode.ExecutionAndPublication); + /// The Reactive attribute definition property name. + private const string ReactiveAttributeName = "ReactiveAttribute"; + + /// The generated hint name for the Reactive attribute. + private const string ReactiveAttributeHintName = "ReactiveAttribute.g.cs"; + + /// The fully qualified type name containing attribute definitions. + private const string AttributeDefinitionsTypeName = "ReactiveUI.SourceGenerators.Helpers.AttributeDefinitions"; + /// - /// Gets the verified file path for generator type . + /// Cache support references per generator type T. The support assembly compiles attribute + /// definitions that are NOT injected by T via RegisterPostInitializationOutput — an expensive + /// Roslyn compilation + Emit step that produces an identical result for every test in the same + /// generator class. Compute it once and reuse it for all subsequent tests. /// + private static readonly Lazy> supportReferences = + new(CreateSupportReferences, LazyThreadSafetyMode.ExecutionAndPublication); + + /// The concrete generator type name used for snapshots and support assemblies. + private static readonly string generatorTypeName = new T().GetType().Name; + + /// Gets the verified file path for generator type . /// /// A string. /// - public string VerifiedFilePath() - { - var name = typeof(T).Name; - return name switch - { - nameof(ReactiveGenerator) => "REACTIVE", - nameof(ReactiveCommandGenerator) => "REACTIVECMD", - nameof(ObservableAsPropertyGenerator) => "OAPH", - nameof(IViewForGenerator) => "IVIEWFOR", - nameof(RoutedControlHostGenerator) => "ROUTEDHOST", - nameof(ViewModelControlHostGenerator) => "CONTROLHOST", - nameof(BindableDerivedListGenerator) => "DERIVEDLIST", - nameof(ReactiveCollectionGenerator) => "REACTIVECOLL", - nameof(ReactiveObjectGenerator) => "REACTIVEOBJ", - _ => name, - }; - } + public string VerifiedFilePath() => GetVerifiedFilePath(); - /// - /// Asynchronously initializes the source generator helper. - /// + /// Asynchronously initializes the source generator helper. /// A task representing the completed initialization operation. public Task InitializeAsync() => Task.CompletedTask; - /// - /// Tests a generator expecting it to fail by throwing an . - /// + /// Tests a generator expecting it to fail by throwing an . /// The source code to test. /// A task representing the asynchronous assertion operation. - public Task TestFail( - string source) - { -#pragma warning disable IDE0053 // Use expression body for lambda expression -#pragma warning disable RCS1021 // Convert lambda expression body to expression body - Assert.Throws(() => { RunGeneratorAndCheck(source); }); -#pragma warning restore RCS1021 // Convert lambda expression body to expression body -#pragma warning restore IDE0053 // Use expression body for lambda expression + public async Task TestFail(string source) => + await Assert.That(() => RunGeneratorAndCheck(source)).Throws(); - return Task.CompletedTask; - } + /// Tests a generator expecting it to pass successfully. + /// The source code to test. + /// A task representing the asynchronous verification operation. + public Task TestPass(string source) => + TestPass(source, withPreDiagnosics: false); - /// - /// Tests a generator expecting it to pass successfully. - /// + /// Tests a generator expecting it to pass successfully. /// The source code to test. /// if set to true [with pre diagnosics]. /// A task representing the asynchronous verification operation. - /// Must have valid compiler instance. - /// callerType. - public Task TestPass( - string source, - bool withPreDiagnosics = false) - => RunGeneratorAndCheck(source, withPreDiagnosics); + public Task TestPass(string source, bool withPreDiagnosics) => + RunGeneratorAndCheck(source, withPreDiagnosics); /// public void Dispose() { } - /// - /// Runs the specified source generator and validates the generated code. - /// + /// Runs the specified source generator and validates the generated code. + /// The code to be parsed and processed by the generator. + /// The generator driver used to run the generator. + /// Thrown if the compilation fails. + public SettingsTask RunGeneratorAndCheck(string code) => + RunGeneratorAndCheck(code, withPreDiagnosics: false, rerunCompilation: true); + + /// Runs the specified source generator and validates the generated code. + /// The code to be parsed and processed by the generator. + /// if set to true [with pre diagnosics]. + /// The generator driver used to run the generator. + public SettingsTask RunGeneratorAndCheck(string code, bool withPreDiagnosics) => + RunGeneratorAndCheck(code, withPreDiagnosics, rerunCompilation: true); + + /// Runs the specified source generator and validates the generated code. /// The code to be parsed and processed by the generator. /// if set to true [with pre diagnosics]. /// Indicates whether to rerun the compilation after running the generator. - /// - /// The generator driver used to run the generator. - /// + /// The generator driver used to run the generator. /// Thrown if the compilation fails. public SettingsTask RunGeneratorAndCheck( string code, - bool withPreDiagnosics = false, - bool rerunCompilation = true) + bool withPreDiagnosics, + bool rerunCompilation) { - // Collect required assembly references: runtime assemblies plus a support assembly - // that provides attribute/enum definitions for generators OTHER than the active generator T. - // Generator T injects its own definitions via RegisterPostInitializationOutput, so those - // are excluded from the support assembly to avoid CS0433 duplicate-type errors. - var assemblies = new HashSet( - TestCompilationReferences.CreateDefault().Concat(supportReferences.Value)); - var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13); - var syntaxTrees = new List - { - // Mirror the test project's GlobalUsings.g.cs so test sources can use unqualified - // attribute names (e.g. [BindableDerivedList]) without an explicit 'using' directive. - CSharpSyntaxTree.ParseText( - "global using ReactiveUI.SourceGenerators;", - parseOptions, - path: "GlobalUsings.g.cs"), - CSharpSyntaxTree.ParseText(code, parseOptions), - }; - - // When the active generator is NOT ReactiveGenerator, the shared enum types - // (AccessModifier, PropertyAccessModifier, InheritanceModifier, SplatRegistrationType) - // are not injected by any generator but may be referenced by test source code or by the - // generator's own output. Add them directly as source trees so they are visible in both - // the input and output compilations at the correct (non-internal) accessibility level. - if (typeof(T) != typeof(ReactiveGenerator)) - { - syntaxTrees.Add(CSharpSyntaxTree.ParseText( - GetAttributeDefinitionsMethodResult("GetAccessModifierEnum"), - parseOptions, - path: "AccessModifierEnum.g.cs")); - } - - // When the active generator is IViewForGenerator, the [Reactive] and [ReactiveCommand] - // attributes are not injected (those belong to ReactiveGenerator and ReactiveCommandGenerator). - // They are also excluded from the support DLL (above), so add them directly as inline source - // trees — this makes them visible in the test source compilation without CS0122. - if (typeof(T) == typeof(IViewForGenerator)) - { - syntaxTrees.Add(CSharpSyntaxTree.ParseText( - GetAttributeDefinitionsPropertyResult("ReactiveAttribute"), - parseOptions, - path: "ReactiveAttribute.g.cs")); - syntaxTrees.Add(CSharpSyntaxTree.ParseText( - GetAttributeDefinitionsPropertyResult("ReactiveCommandAttribute"), - parseOptions, - path: "ReactiveCommandAttribute.g.cs")); - } - - // When the active generator is ReactiveObjectGenerator, [Reactive] and [ObservableAsProperty] - // attributes are not injected by this generator (they belong to ReactiveGenerator and - // ObservableAsPropertyGenerator). They are excluded from the support DLL to avoid CS0433, - // so add them directly as inline source trees for accessibility. - if (typeof(T) == typeof(ReactiveObjectGenerator)) - { - syntaxTrees.Add(CSharpSyntaxTree.ParseText( - GetAttributeDefinitionsPropertyResult("ReactiveAttribute"), - parseOptions, - path: "ReactiveAttribute.g.cs")); - syntaxTrees.Add(CSharpSyntaxTree.ParseText( - GetAttributeDefinitionsPropertyResult("ObservableAsPropertyAttribute"), - parseOptions, - path: "ObservableAsPropertyAttribute.g.cs")); - } - - // BindableDerivedListGenerator and ReactiveCollectionGenerator inject their own attribute - // via RegisterPostInitializationOutput. Tests that also use [Reactive] (WithReactive tests) - // need ReactiveAttribute as an inline source tree because it is excluded from the support DLL. - if (typeof(T) == typeof(BindableDerivedListGenerator) || typeof(T) == typeof(ReactiveCollectionGenerator)) - { - syntaxTrees.Add(CSharpSyntaxTree.ParseText( - GetAttributeDefinitionsPropertyResult("ReactiveAttribute"), - parseOptions, - path: "ReactiveAttribute.g.cs")); - } - - // On non-Windows platforms the Microsoft.WindowsDesktop.App shared framework is unavailable, - // so test sources that inherit from System.Windows.Window (WPF) or use Windows Forms types - // cannot resolve those types from assembly references. Inject lightweight source stubs so - // the in-memory compilation succeeds cross-platform. - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - syntaxTrees.Add(CSharpSyntaxTree.ParseText( - TestCompilationReferences.WindowsDesktopStubs, - parseOptions, - path: "WindowsDesktopStubs.g.cs")); - } - - // Create a compilation with the provided source code. - var compilation = CSharpCompilation.Create( - "TestProject", - syntaxTrees, - assemblies, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true)); + var compilation = CreateTestCompilation(code, parseOptions); if (withPreDiagnosics) { // Validate diagnostics before running the generator. - var prediagnostics = compilation.GetDiagnostics() - .Where(d => d.Severity > DiagnosticSeverity.Warning) - .ToList(); + var prediagnostics = GetDiagnosticsAboveSeverity(compilation.GetDiagnostics(), DiagnosticSeverity.Warning); if (prediagnostics.Count > 0) { @@ -219,261 +119,516 @@ public SettingsTask RunGeneratorAndCheck( var generator = new T(); var driver = CSharpGeneratorDriver.Create(generator).WithUpdatedParseOptions(parseOptions); - if (rerunCompilation) + return rerunCompilation + ? RunGeneratorAndVerify(code, driver, compilation) + : VerifyGenerator(driver.RunGenerators(compilation)); + } + + /// Gets the verified file path for generator type . + /// The snapshot directory name. + private static string GetVerifiedFilePath() + { + var name = generatorTypeName; + return name switch { - // Run the generator and capture diagnostics. - var rerunDriver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + nameof(ReactiveGenerator) => "REACTIVE", + nameof(ReactiveCommandGenerator) => "REACTIVECMD", + nameof(ObservableAsPropertyGenerator) => "OAPH", + nameof(IViewForGenerator) => "IVIEWFOR", + nameof(RoutedControlHostGenerator) => "ROUTEDHOST", + nameof(ViewModelControlHostGenerator) => "CONTROLHOST", + nameof(BindableDerivedListGenerator) => "DERIVEDLIST", + nameof(ReactiveCollectionGenerator) => "REACTIVECOLL", + nameof(ReactiveObjectGenerator) => "REACTIVEOBJ", + _ => name, + }; + } - // If any warnings or errors are found, log them to the test output before throwing an exception. - var offendingDiagnostics = diagnostics - .Where(d => d.Severity >= DiagnosticSeverity.Warning) - .ToList(); + /// Creates an in-memory compilation containing the supplied test code. + /// The test code to compile. + /// The language version settings. + /// The prepared Roslyn compilation. + private static CSharpCompilation CreateTestCompilation(string code, CSharpParseOptions parseOptions) + { + var syntaxTrees = new List + { + CSharpSyntaxTree.ParseText("global using ReactiveUI.SourceGenerators;", parseOptions, path: "GlobalUsings.g.cs"), + CSharpSyntaxTree.ParseText(code, parseOptions), + }; - if (offendingDiagnostics.Count > 0) - { - foreach (var diagnostic in offendingDiagnostics) - { - WriteTestOutput($"Diagnostic: {diagnostic.Id} - {diagnostic.GetMessage()}"); - } + AddGeneratorSpecificSyntaxTrees(syntaxTrees, parseOptions); + AddWindowsDesktopStubsWhenNeeded(syntaxTrees, parseOptions); + return CSharpCompilation.Create("TestProject", syntaxTrees, CreateAssemblyReferences(), new(OutputKind.DynamicallyLinkedLibrary, deterministic: true)); + } - throw new InvalidOperationException("Compilation failed due to the above diagnostics."); - } + /// Adds source trees required by the active generator. + /// The source-tree collection to extend. + /// The language version settings. + private static void AddGeneratorSpecificSyntaxTrees(List syntaxTrees, CSharpParseOptions parseOptions) + { + if (typeof(T) != typeof(ReactiveGenerator)) + { + AddSyntaxTree(syntaxTrees, GetAttributeDefinitionsMethodResult("GetAccessModifierEnum"), parseOptions, "AccessModifierEnum.g.cs"); + } - var outputDiagnosticsToReport = outputCompilation.GetDiagnostics() - .Where(d => d.Severity >= DiagnosticSeverity.Error) - .Where(d => !IsKnownExpectedOutputDiagnostic(d)) - .ToList(); + if (typeof(T) == typeof(IViewForGenerator)) + { + AddSyntaxTree(syntaxTrees, GetAttributeDefinitionsPropertyResult(ReactiveAttributeName), parseOptions, ReactiveAttributeHintName); + AddSyntaxTree(syntaxTrees, GetAttributeDefinitionsPropertyResult("ReactiveCommandAttribute"), parseOptions, "ReactiveCommandAttribute.g.cs"); + } - if (outputDiagnosticsToReport.Count > 0) - { - var diagnosticMessage = string.Join(Environment.NewLine, outputDiagnosticsToReport.Select(static d => $"{d.Id} - {d.GetMessage()}")); + if (typeof(T) == typeof(ReactiveObjectGenerator)) + { + AddSyntaxTree(syntaxTrees, GetAttributeDefinitionsPropertyResult(ReactiveAttributeName), parseOptions, ReactiveAttributeHintName); + AddSyntaxTree(syntaxTrees, GetAttributeDefinitionsPropertyResult("ObservableAsPropertyAttribute"), parseOptions, "ObservableAsPropertyAttribute.g.cs"); + } - foreach (var diagnostic in outputDiagnosticsToReport) - { - WriteTestOutput($"Output diagnostic: {diagnostic.Id} - {diagnostic.GetMessage()}"); - } + if (typeof(T) != typeof(BindableDerivedListGenerator) && typeof(T) != typeof(ReactiveCollectionGenerator)) + { + return; + } - throw new InvalidOperationException($"Output compilation failed due to the above diagnostics.{Environment.NewLine}{diagnosticMessage}"); - } + AddSyntaxTree(syntaxTrees, GetAttributeDefinitionsPropertyResult(ReactiveAttributeName), parseOptions, ReactiveAttributeHintName); + } + + /// Adds Windows desktop stubs when the operating system does not provide them. + /// The source-tree collection to extend. + /// The language version settings. + private static void AddWindowsDesktopStubsWhenNeeded(List syntaxTrees, CSharpParseOptions parseOptions) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } - // Validate generated code contains expected features - ValidateGeneratedCode(code, rerunDriver); + AddSyntaxTree(syntaxTrees, TestCompilationReferences.WindowsDesktopStubs, parseOptions, "WindowsDesktopStubs.g.cs"); + } + + /// Adds a parsed source tree to a syntax-tree collection. + /// The source-tree collection to extend. + /// The source code to parse. + /// The language version settings. + /// The generated source path. + private static void AddSyntaxTree(List syntaxTrees, string source, CSharpParseOptions parseOptions, string path) => + syntaxTrees.Add(CSharpSyntaxTree.ParseText(source, parseOptions, path: path)); + + /// Runs a generator and validates all resulting diagnostics and source. + /// The original source code. + /// The configured generator driver. + /// The input compilation. + /// The snapshot verification settings. + private static SettingsTask RunGeneratorAndVerify(string code, GeneratorDriver driver, Compilation compilation) + { + var rerunDriver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + ThrowIfDiagnosticsExist(GetDiagnosticsAtLeastSeverity(diagnostics, DiagnosticSeverity.Warning), "Diagnostic", "Compilation failed due to the above diagnostics."); + ThrowIfDiagnosticsExist(GetUnexpectedOutputDiagnostics(outputCompilation.GetDiagnostics()), "Output diagnostic", "Output compilation failed due to the above diagnostics."); + ValidateGeneratedCode(code, rerunDriver); + return VerifyGenerator(rerunDriver); + } - return VerifyGenerator(rerunDriver); + /// Writes diagnostics and throws when a collection is non-empty. + /// The diagnostics to evaluate. + /// The output prefix. + /// The exception message prefix. + private static void ThrowIfDiagnosticsExist(List diagnostics, string prefix, string failureMessage) + { + if (diagnostics.Count == 0) + { + return; } - // If rerun is not needed, simply run the generator. - return VerifyGenerator(driver.RunGenerators(compilation)); + foreach (var diagnostic in diagnostics) + { + WriteTestOutput($"{prefix}: {diagnostic.Id} - {diagnostic.GetMessage()}"); + } + + throw new InvalidOperationException($"{failureMessage}{Environment.NewLine}{CreateDiagnosticMessage(diagnostics)}"); } - /// - /// Returns all attribute/enum source strings that are NOT already injected by generator T - /// via RegisterPostInitializationOutput. Including sources that the active generator also - /// emits would create CS0433 (duplicate type) in the output compilation. - /// - private static IEnumerable GetGeneratedSupportSources() + /// Returns attribute and enum source strings not injected by generator . + /// The source strings required by the support assembly. + private static List GetGeneratedSupportSources() { - // Always include the shared enum block (AccessModifier, PropertyAccessModifier, - // InheritanceModifier, SplatRegistrationType). These are internal types so they - // live inside the support-assembly DLL and never cause CS0433 conflicts, even when - // ReactiveGenerator also injects them into the test compilation as source. - // Omitting this block breaks ReactiveCommandAttribute (needs PropertyAccessModifier) - // and IViewForAttribute (needs SplatRegistrationType). - yield return GetAttributeDefinitionsMethodResult("GetAccessModifierEnum"); + var supportSources = new List { GetAttributeDefinitionsMethodResult("GetAccessModifierEnum") }; + AddRequiredAttributeDefinitions(supportSources); + return supportSources; + } + + /// Adds attribute definitions required before the common definitions. + /// The support-source collection to extend. + private static void AddRequiredAttributeDefinitions(List supportSources) + { // Yield each attribute definition only if generator T does NOT inject it. // Note: for IViewForGenerator, ReactiveAttribute and ReactiveCommandAttribute are // added as inline SyntaxTrees below (not in the support DLL) so they are accessible // in the test source compilation without CS0122 internal-visibility errors. if (typeof(T) != typeof(ReactiveCommandGenerator) && typeof(T) != typeof(IViewForGenerator)) { - yield return GetAttributeDefinitionsPropertyResult("ReactiveCommandAttribute"); + supportSources.Add(GetAttributeDefinitionsPropertyResult("ReactiveCommandAttribute")); } - if (typeof(T) != typeof(ReactiveGenerator) && typeof(T) != typeof(IViewForGenerator) && typeof(T) != typeof(ReactiveObjectGenerator) - && typeof(T) != typeof(BindableDerivedListGenerator) && typeof(T) != typeof(ReactiveCollectionGenerator)) - { - yield return GetAttributeDefinitionsPropertyResult("ReactiveAttribute"); - } + AddReactiveAttributeDefinitionIfNeeded(supportSources); if (typeof(T) != typeof(IViewForGenerator)) { - yield return GetAttributeDefinitionsPropertyResult("IViewForAttribute"); + supportSources.Add(GetAttributeDefinitionsPropertyResult("IViewForAttribute")); } if (typeof(T) != typeof(ObservableAsPropertyGenerator) && typeof(T) != typeof(ReactiveObjectGenerator)) { - yield return GetAttributeDefinitionsPropertyResult("ObservableAsPropertyAttribute"); + supportSources.Add(GetAttributeDefinitionsPropertyResult("ObservableAsPropertyAttribute")); + } + + AddRemainingAttributeDefinitions(supportSources); + } + + /// Adds the Reactive attribute definition when the active generator does not provide it. + /// The support-source collection to extend. + private static void AddReactiveAttributeDefinitionIfNeeded(List supportSources) + { + if (typeof(T) == typeof(ReactiveGenerator) || typeof(T) == typeof(IViewForGenerator) || typeof(T) == typeof(ReactiveObjectGenerator) + || typeof(T) == typeof(BindableDerivedListGenerator) || typeof(T) == typeof(ReactiveCollectionGenerator)) + { + return; } + supportSources.Add(GetAttributeDefinitionsPropertyResult(ReactiveAttributeName)); + } + + /// Adds the remaining conditional attribute definitions. + /// The support-source collection to extend. + private static void AddRemainingAttributeDefinitions(List supportSources) + { if (typeof(T) != typeof(BindableDerivedListGenerator)) { - yield return GetAttributeDefinitionsPropertyResult("BindableDerivedListAttribute"); + supportSources.Add(GetAttributeDefinitionsPropertyResult("BindableDerivedListAttribute")); } if (typeof(T) != typeof(ReactiveCollectionGenerator)) { - yield return GetAttributeDefinitionsPropertyResult("ReactiveCollectionAttribute"); + supportSources.Add(GetAttributeDefinitionsPropertyResult("ReactiveCollectionAttribute")); } if (typeof(T) != typeof(ReactiveObjectGenerator)) { - yield return GetAttributeDefinitionsPropertyResult("ReactiveObjectAttribute"); + supportSources.Add(GetAttributeDefinitionsPropertyResult("ReactiveObjectAttribute")); } if (typeof(T) != typeof(RoutedControlHostGenerator)) { - yield return GetAttributeDefinitionsMethodResult("GetRoutedControlHostAttribute"); + supportSources.Add(GetAttributeDefinitionsMethodResult("GetRoutedControlHostAttribute")); } - if (typeof(T) != typeof(ViewModelControlHostGenerator)) + if (typeof(T) == typeof(ViewModelControlHostGenerator)) { - yield return GetAttributeDefinitionsPropertyResult("ViewModelControlHostAttribute"); + return; } + + supportSources.Add(GetAttributeDefinitionsPropertyResult("ViewModelControlHostAttribute")); } + /// Creates metadata references for the support source assembly. + /// The references that provide support attributes and enums. private static ImmutableArray CreateSupportReferences() { - var supportSources = GetGeneratedSupportSources().ToArray(); - if (supportSources.Length == 0) + var supportSources = GetGeneratedSupportSources(); + + if (supportSources.Count == 0) { return []; } var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13); var supportCompilation = CSharpCompilation.Create( - $"{typeof(T).Name}.Support", - supportSources.Select((source, index) => CSharpSyntaxTree.ParseText(source, parseOptions, path: $"Support{index}.g.cs")), + $"{generatorTypeName}.Support", + CreateSupportSyntaxTrees(supportSources, parseOptions), TestCompilationReferences.CreateDefault(), - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true)); + new(OutputKind.DynamicallyLinkedLibrary, deterministic: true)); using var stream = new MemoryStream(); var emitResult = supportCompilation.Emit(stream); if (!emitResult.Success) { - var diagnostics = string.Join(Environment.NewLine, emitResult.Diagnostics.Select(static d => d.ToString())); - throw new InvalidOperationException($"Support assembly compilation failed for {typeof(T).Name}.{Environment.NewLine}{diagnostics}"); + var diagnostics = CreateDiagnosticMessage(emitResult.Diagnostics); + throw new InvalidOperationException($"Support assembly compilation failed for {generatorTypeName}.{Environment.NewLine}{diagnostics}"); } return [MetadataReference.CreateFromImage(stream.ToArray())]; } + /// Creates the metadata references used by an in-memory test compilation. + /// The default and generator support metadata references. + private static HashSet CreateAssemblyReferences() + { + var references = new HashSet(TestCompilationReferences.CreateDefault()); + references.UnionWith(supportReferences.Value); + return references; + } + + /// Creates syntax trees for the supplied support source strings. + /// The support source strings. + /// The language version settings. + /// The parsed support syntax trees. + private static List CreateSupportSyntaxTrees(List supportSources, CSharpParseOptions parseOptions) + { + var syntaxTrees = new List(supportSources.Count); + for (var index = 0; index < supportSources.Count; index++) + { + syntaxTrees.Add(CSharpSyntaxTree.ParseText(supportSources[index], parseOptions, path: $"Support{index}.g.cs")); + } + + return syntaxTrees; + } + + /// Gets a public attribute-definition method result. + /// The public static method name. + /// The generated source returned by the method. private static string GetAttributeDefinitionsMethodResult(string methodName) { - var attributeDefinitionsType = typeof(ReactiveGenerator).Assembly.GetType("ReactiveUI.SourceGenerators.Helpers.AttributeDefinitions", throwOnError: false, ignoreCase: false) + var attributeDefinitionsType = typeof(ReactiveGenerator).Assembly.GetType(AttributeDefinitionsTypeName, throwOnError: false, ignoreCase: false) ?? throw new InvalidOperationException("Could not locate AttributeDefinitions type."); - var method = attributeDefinitionsType.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + var method = attributeDefinitionsType.GetMethod(methodName) ?? throw new InvalidOperationException($"Could not locate AttributeDefinitions.{methodName}."); - return (string?)method.Invoke(null, null) + var result = method.Invoke(null, null); + return (string?)result ?? throw new InvalidOperationException($"AttributeDefinitions.{methodName} returned null."); } + /// Gets a public attribute-definition property result. + /// The public static property name. + /// The generated source returned by the property. private static string GetAttributeDefinitionsPropertyResult(string propertyName) { - var attributeDefinitionsType = typeof(ReactiveGenerator).Assembly.GetType("ReactiveUI.SourceGenerators.Helpers.AttributeDefinitions", throwOnError: false, ignoreCase: false) + var attributeDefinitionsType = typeof(ReactiveGenerator).Assembly.GetType(AttributeDefinitionsTypeName, throwOnError: false, ignoreCase: false) ?? throw new InvalidOperationException("Could not locate AttributeDefinitions type."); - var property = attributeDefinitionsType.GetProperty(propertyName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + var property = attributeDefinitionsType.GetProperty(propertyName) ?? throw new InvalidOperationException($"Could not locate AttributeDefinitions.{propertyName}."); return (string?)property.GetValue(null) ?? throw new InvalidOperationException($"AttributeDefinitions.{propertyName} returned null."); } + /// Determines whether a diagnostic is an accepted generated-output diagnostic. + /// The diagnostic to inspect. + /// when the diagnostic is expected. private static bool IsKnownExpectedOutputDiagnostic(Diagnostic d) => d.Id is "CS0579" or "CS8864" or "CS0115" or "CS8867" or "CS8866"; + /// Filters diagnostics more severe than a specified level. + /// The diagnostics to inspect. + /// The exclusive severity threshold. + /// Diagnostics exceeding the threshold. + private static List GetDiagnosticsAboveSeverity(IEnumerable diagnostics, DiagnosticSeverity severity) + { + var result = new List(); + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Severity > severity) + { + result.Add(diagnostic); + } + } + + return result; + } + + /// Filters diagnostics at or above a specified severity level. + /// The diagnostics to inspect. + /// The inclusive severity threshold. + /// Diagnostics meeting the threshold. + private static List GetDiagnosticsAtLeastSeverity(IEnumerable diagnostics, DiagnosticSeverity severity) + { + var result = new List(); + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Severity >= severity) + { + result.Add(diagnostic); + } + } + + return result; + } + + /// Filters error diagnostics that are not expected generator output diagnostics. + /// The diagnostics to inspect. + /// Unexpected error diagnostics. + private static List GetUnexpectedOutputDiagnostics(IEnumerable diagnostics) + { + var result = new List(); + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Severity >= DiagnosticSeverity.Error && !IsKnownExpectedOutputDiagnostic(diagnostic)) + { + result.Add(diagnostic); + } + } + + return result; + } + + /// Formats diagnostics for test-output reporting. + /// The diagnostics to format. + /// A newline-delimited diagnostic message. + private static string CreateDiagnosticMessage(IEnumerable diagnostics) + { + var messages = new List(); + foreach (var diagnostic in diagnostics) + { + messages.Add($"{diagnostic.Id} - {diagnostic.GetMessage()}"); + } + + return string.Join(Environment.NewLine, messages); + } + [GeneratedRegex(@"\[Reactive\((?:.*?nameof\((\w+)\))+", RegexOptions.Singleline)] private static partial Regex ReactiveRegex(); [GeneratedRegex(@"nameof\((\w+)\)")] private static partial Regex NameOfRegex(); - /// - /// Validates that generated code contains expected features based on the source code attributes. - /// + /// Validates that generated code contains expected features based on the source code attributes. /// The original source code. /// The generator driver with generated output. private static void ValidateGeneratedCode(string sourceCode, GeneratorDriver driver) { var runResult = driver.GetRunResult(); - var generatedTrees = runResult.Results.SelectMany(r => r.GeneratedSources).ToList(); - var allGeneratedCode = string.Join("\n", generatedTrees.Select(t => t.SourceText.ToString())); + var generatedTrees = GetGeneratedSources(runResult); + var allGeneratedCode = GetGeneratedCode(generatedTrees); - if (typeof(T) == typeof(ReactiveCommandGenerator)) + ValidateReactiveCommandOutput(generatedTrees); + ValidateAlsoNotifyOutput(sourceCode, allGeneratedCode); + } + + /// Gets every generated source from a generator run result. + /// The result to inspect. + /// The generated sources from all generators. + private static List GetGeneratedSources(GeneratorDriverRunResult runResult) + { + var generatedSources = new List(); + foreach (var result in runResult.Results) { - var hasReactiveCommandOutput = generatedTrees.Any(static s => s.HintName.EndsWith(".ReactiveCommands.g.cs", StringComparison.Ordinal)); + generatedSources.AddRange(result.GeneratedSources); + } - if (!hasReactiveCommandOutput) - { - WriteTestOutput("=== VALIDATION FAILURE ==="); - WriteTestOutput("ReactiveCommand generator produced no command source output."); - WriteTestOutput("=== GENERATED HINTS ==="); + return generatedSources; + } - foreach (var generatedTree in generatedTrees) - { - WriteTestOutput(generatedTree.HintName); - } + /// Combines generated source text for validation. + /// The sources to combine. + /// The combined generated source code. + private static string GetGeneratedCode(List generatedSources) + { + var generatedCode = new StringBuilder(); + foreach (var generatedSource in generatedSources) + { + _ = generatedCode.AppendLine(generatedSource.SourceText.ToString()); + } + + return generatedCode.ToString(); + } - WriteTestOutput("=== END ==="); + /// Ensures the command generator emits its command source. + /// The generated sources to inspect. + private static void ValidateReactiveCommandOutput(List generatedSources) + { + if (typeof(T) != typeof(ReactiveCommandGenerator)) + { + return; + } - throw new InvalidOperationException("ReactiveCommand generator produced no command source output."); + foreach (var generatedSource in generatedSources) + { + if (generatedSource.HintName.EndsWith(".ReactiveCommands.g.cs", StringComparison.Ordinal)) + { + return; } } - // Check for AlsoNotify feature in Reactive attributes - // Pattern matches: [Reactive(nameof(PropertyName))] or [Reactive(nameof(Prop1), nameof(Prop2))] - var alsoNotifyPattern = ReactiveRegex(); - var nameofPattern = NameOfRegex(); - var matches = alsoNotifyPattern.Matches(sourceCode); + WriteTestOutput("=== VALIDATION FAILURE ==="); + WriteTestOutput("ReactiveCommand generator produced no command source output."); + WriteTestOutput("=== GENERATED HINTS ==="); + foreach (var generatedSource in generatedSources) + { + WriteTestOutput(generatedSource.HintName); + } - if (matches.Count > 0) + WriteTestOutput("=== END ==="); + throw new InvalidOperationException("ReactiveCommand generator produced no command source output."); + } + + /// Ensures reactive attributes produce their requested additional notifications. + /// The original source code. + /// The combined generated code. + private static void ValidateAlsoNotifyOutput(string sourceCode, string allGeneratedCode) + { + foreach (object? matchValue in ReactiveRegex().Matches(sourceCode)) { - foreach (Match match in matches) + if (matchValue is not Match match) { - // Extract all nameof() references within this attribute - var nameofMatches = nameofPattern.Matches(match.Value); + continue; + } - foreach (Match nameofMatch in nameofMatches) + foreach (object? nameofMatchValue in NameOfRegex().Matches(match.Value)) + { + if (nameofMatchValue is not Match nameofMatch) { - var propertyToNotify = nameofMatch.Groups[1].Value; - - // Verify that the generated code contains calls to raise property changed for the additional property - // Check for various forms of property change notification - var hasNotification = - allGeneratedCode.Contains($"this.RaisePropertyChanged(nameof({propertyToNotify}))") || - allGeneratedCode.Contains($"this.RaisePropertyChanged(\"{propertyToNotify}\")") || - allGeneratedCode.Contains($"RaisePropertyChanged(nameof({propertyToNotify}))") || - allGeneratedCode.Contains($"RaisePropertyChanged(\"{propertyToNotify}\")"); - - if (!hasNotification) - { - var errorMessage = $"Generated code does not include AlsoNotify for property '{propertyToNotify}'. " + - $"Expected to find property change notification for '{propertyToNotify}' in the generated code.\n" + - $"Source attribute: {match.Value}"; - - WriteTestOutput("=== VALIDATION FAILURE ==="); - WriteTestOutput(errorMessage); - WriteTestOutput("=== SOURCE CODE SNIPPET ==="); - WriteTestOutput(match.Value); - WriteTestOutput("=== GENERATED CODE ==="); - WriteTestOutput(allGeneratedCode); - WriteTestOutput("=== END ==="); - - throw new InvalidOperationException(errorMessage); - } + continue; } + + ValidatePropertyNotification(nameofMatch.Groups[1].Value, match.Value, allGeneratedCode); } } } + /// Ensures generated code raises a requested property notification. + /// The requested property name. + /// The source attribute that requested it. + /// The combined generated code. + private static void ValidatePropertyNotification(string propertyToNotify, string sourceAttribute, string allGeneratedCode) + { + if (ContainsPropertyNotification(allGeneratedCode, propertyToNotify)) + { + return; + } + + var errorMessage = $"Generated code does not include AlsoNotify for property '{propertyToNotify}'. " + + $"Expected to find property change notification for '{propertyToNotify}' in the generated code.{Environment.NewLine}" + + $"Source attribute: {sourceAttribute}"; + WriteTestOutput("=== VALIDATION FAILURE ==="); + WriteTestOutput(errorMessage); + WriteTestOutput("=== SOURCE CODE SNIPPET ==="); + WriteTestOutput(sourceAttribute); + WriteTestOutput("=== GENERATED CODE ==="); + WriteTestOutput(allGeneratedCode); + WriteTestOutput("=== END ==="); + throw new InvalidOperationException(errorMessage); + } + + /// Determines whether generated code contains a requested notification. + /// The generated code. + /// The property name. + /// when the notification appears. + private static bool ContainsPropertyNotification(string generatedCode, string propertyName) => + generatedCode.Contains($"this.RaisePropertyChanged(nameof({propertyName}))", StringComparison.Ordinal) + || generatedCode.Contains($"this.RaisePropertyChanged(\"{propertyName}\")", StringComparison.Ordinal) + || generatedCode.Contains($"RaisePropertyChanged(nameof({propertyName}))", StringComparison.Ordinal) + || generatedCode.Contains($"RaisePropertyChanged(\"{propertyName}\")", StringComparison.Ordinal); + + /// Writes a validation message to the current test output. + /// The message to write. private static void WriteTestOutput(string message) => TestContext.Current?.OutputWriter.WriteLine(message); - private SettingsTask VerifyGenerator(GeneratorDriver driver) - => Verify(driver) - .UseDirectory(VerifiedFilePath()) + /// Creates snapshot verification settings for a generator driver. + /// The generator driver to verify. + /// The snapshot verification settings. + private static SettingsTask VerifyGenerator(GeneratorDriver driver) => + Verify(driver) + .UseDirectory(GetVerifiedFilePath()) .ScrubLinesContaining("[global::System.CodeDom.Compiler.GeneratedCode(\""); } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs index a4880e1b..e6acc1b0 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs @@ -1,20 +1,22 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for . -/// +/// Extended unit tests for . public sealed class AttrMisuseExtTests { - /// - /// Verifies a non-partial property with [Reactive] in a partial type produces a warning. - /// + /// Identifies the diagnostic validated by this test class. + private const string ReactivePartialDiagnosticId = "RXUISG0020"; + + /// Defines the expected number of diagnostics for the multi-diagnostic test. + private const int ExpectedDiagnosticCount = 3; + + /// Verifies a non-partial property with [Reactive] in a partial type produces a warning. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnNonPartialPropertyInPartialTypeThenWarn() + public async Task WhenReactiveOnNonPartialPropertyInPartialTypeThenWarn() { const string source = """ using ReactiveUI; @@ -29,16 +31,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies a partial property with [Reactive] in a non-partial type produces a warning. - /// + /// Verifies a partial property with [Reactive] in a non-partial type produces a warning. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnPartialPropertyInNonPartialTypeThenWarn() + public async Task WhenReactiveOnPartialPropertyInNonPartialTypeThenWarn() { const string source = """ using ReactiveUI; @@ -53,16 +54,15 @@ public class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies no warning when both property and type are partial. - /// + /// Verifies no warning when both property and type are partial. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnPartialPropertyInPartialTypeThenNoWarn() + public async Task WhenReactiveOnPartialPropertyInPartialTypeThenNoWarn() { const string source = """ using ReactiveUI; @@ -77,16 +77,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning in nested non-partial class. - /// + /// Verifies warning in nested non-partial class. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveInNestedNonPartialClassThenWarn() + public async Task WhenReactiveInNestedNonPartialClassThenWarn() { const string source = """ using ReactiveUI; @@ -104,16 +103,15 @@ public class InnerVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies no warning in nested partial class with partial property. - /// + /// Verifies no warning in nested partial class with partial property. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveInNestedPartialClassWithPartialPropertyThenNoWarn() + public async Task WhenReactiveInNestedPartialClassWithPartialPropertyThenNoWarn() { const string source = """ using ReactiveUI; @@ -131,16 +129,15 @@ public partial class InnerVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning for non-partial record. - /// + /// Verifies warning for non-partial record. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveInNonPartialRecordThenWarn() + public async Task WhenReactiveInNonPartialRecordThenWarn() { const string source = """ using ReactiveUI; @@ -155,16 +152,15 @@ public record TestVMRecord : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies no warning for partial record with partial property. - /// + /// Verifies no warning for partial record with partial property. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveInPartialRecordWithPartialPropertyThenNoWarn() + public async Task WhenReactiveInPartialRecordWithPartialPropertyThenNoWarn() { const string source = """ using ReactiveUI; @@ -179,16 +175,15 @@ public partial record TestVMRecord : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning for multiple non-partial properties. - /// + /// Verifies warning for multiple non-partial properties. + /// A task that represents the asynchronous test operation. [Test] - public void WhenMultipleNonPartialPropertiesWithReactiveThenWarnForEach() + public async Task WhenMultipleNonPartialPropertiesWithReactiveThenWarnForEach() { const string source = """ using ReactiveUI; @@ -209,16 +204,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDiagnosticCount(diagnostics, "RXUISG0020", 3); + await AssertDiagnosticCount(diagnostics, ReactivePartialDiagnosticId, ExpectedDiagnosticCount); } - /// - /// Verifies no warning for field-based [Reactive] attribute. - /// + /// Verifies no warning for field-based [Reactive] attribute. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnFieldThenNoWarn() + public async Task WhenReactiveOnFieldThenNoWarn() { const string source = """ using ReactiveUI; @@ -233,16 +227,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning for deeply nested non-partial types. - /// + /// Verifies warning for deeply nested non-partial types. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveInDeeplyNestedNonPartialTypeThenWarn() + public async Task WhenReactiveInDeeplyNestedNonPartialTypeThenWarn() { const string source = """ using ReactiveUI; @@ -263,16 +256,15 @@ public class Level3 : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies no warning for deeply nested partial types with partial properties. - /// + /// Verifies no warning for deeply nested partial types with partial properties. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveInDeeplyNestedPartialTypesWithPartialPropertyThenNoWarn() + public async Task WhenReactiveInDeeplyNestedPartialTypesWithPartialPropertyThenNoWarn() { const string source = """ using ReactiveUI; @@ -293,16 +285,15 @@ public partial class Level3 : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning when using fully qualified ReactiveAttribute. - /// + /// Verifies warning when using fully qualified ReactiveAttribute. + /// A task that represents the asynchronous test operation. [Test] - public void WhenFullyQualifiedReactiveAttributeOnNonPartialPropertyThenWarn() + public async Task WhenFullyQualifiedReactiveAttributeOnNonPartialPropertyThenWarn() { const string source = """ using ReactiveUI; @@ -316,16 +307,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies no warning for properties without [Reactive] attribute. - /// + /// Verifies no warning for properties without [Reactive] attribute. + /// A task that represents the asynchronous test operation. [Test] - public void WhenNoReactiveAttributeThenNoWarn() + public async Task WhenNoReactiveAttributeThenNoWarn() { const string source = """ using ReactiveUI; @@ -338,16 +328,15 @@ public class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning for generic partial class with non-partial property. - /// + /// Verifies warning for generic partial class with non-partial property. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnNonPartialPropertyInGenericPartialClassThenWarn() + public async Task WhenReactiveOnNonPartialPropertyInGenericPartialClassThenWarn() { const string source = """ using ReactiveUI; @@ -362,16 +351,15 @@ public partial class GenericVM : ReactiveObject where T : class } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies no warning for generic partial class with partial property. - /// + /// Verifies no warning for generic partial class with partial property. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnPartialPropertyInGenericPartialClassThenNoWarn() + public async Task WhenReactiveOnPartialPropertyInGenericPartialClassThenNoWarn() { const string source = """ using ReactiveUI; @@ -386,16 +374,15 @@ public partial class GenericVM : ReactiveObject where T : class } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning for record struct (non-partial). - /// + /// Verifies warning for record struct (non-partial). + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnPropertyInNonPartialRecordStructThenWarn() + public async Task WhenReactiveOnPropertyInNonPartialRecordStructThenWarn() { const string source = """ using ReactiveUI; @@ -410,16 +397,15 @@ public record struct TestVMStruct } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies warning for readonly property with [Reactive] that is not partial. - /// + /// Verifies warning for readonly property with [Reactive] that is not partial. + /// A task that represents the asynchronous test operation. [Test] - public void NonPartialRead() + public async Task NonPartialRead() { const string source = """ using ReactiveUI; @@ -434,14 +420,17 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); // [Reactive] on non-partial property should produce RXUISG0020 // because the attribute requires the property to be partial - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - private static Diagnostic[] GetDiagnostics(string source) + /// Gets diagnostics produced by the analyzer for the supplied source. + /// The source to analyze. + /// A task that resolves to the diagnostics. + private static async Task GetDiagnostics(string source) { var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13)); @@ -452,36 +441,68 @@ private static Diagnostic[] GetDiagnostics(string source) MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), ], - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); var analyzer = new ReactiveAttributeMisuseAnalyzer(); - var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); - return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray(); + var compilationWithAnalyzers = compilation.WithAnalyzers([analyzer]); + return (await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync()).ToArray(); } - private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics contain the specified ID. + /// The diagnostics to inspect. + /// The expected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (!diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsTrue(); } - private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics do not contain the specified ID. + /// The diagnostics to inspect. + /// The unexpected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsFalse(); } - private static void AssertDiagnosticCount(IEnumerable diagnostics, string diagnosticId, int expectedCount) + /// Asserts that the diagnostics contain the expected number of occurrences of an ID. + /// The diagnostics to inspect. + /// The diagnostic ID to count. + /// The expected number of occurrences. + /// A task that represents the assertion. + private static async Task AssertDiagnosticCount(IEnumerable diagnostics, string diagnosticId, int expectedCount) { - var actualCount = diagnostics.Count(d => d.Id == diagnosticId); - if (actualCount != expectedCount) + var actualCount = 0; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Expected {expectedCount} '{diagnosticId}' diagnostics but found {actualCount}."); + if (diagnostic.Id == diagnosticId) + { + actualCount++; + } } + + await Assert.That(actualCount).IsEqualTo(expectedCount); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttributeDataExtensionTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttributeDataExtensionTests.cs index 5879d249..b31a59ff 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttributeDataExtensionTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttributeDataExtensionTests.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -8,16 +7,10 @@ namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for covering -/// TryGetNamedArgument, GetNamedArgument, GetConstructorArguments, -/// and GetGenericType. -/// +/// Unit tests for covering TryGetNamedArgument, GetNamedArgument, GetConstructorArguments, and GetGenericType. public sealed class AttributeDataExtensionTests { - /// - /// TryGetNamedArgument returns true and the correct value when the argument exists. - /// + /// TryGetNamedArgument returns true and the correct value when the argument exists. /// A task to monitor the async. [Test] public async Task WhenNamedArgumentPresentThenTryGetReturnsTrue() @@ -26,24 +19,22 @@ public async Task WhenNamedArgumentPresentThenTryGetReturnsTrue() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeOne : Attribute { - public int Count { get; set; } + public int NamedValueOne { get; set; } } - [MyAttr(Count = 42)] + [AttributeOne(NamedValueOne = 1)] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeOne"); - var found = attribute.TryGetNamedArgument("Count", out int? value); + var found = attribute.TryGetNamedArgument("NamedValueOne", out int? value); await Assert.That(found).IsTrue(); - await Assert.That(value).IsEqualTo(42); + await Assert.That(value).IsEqualTo(1); } - /// - /// TryGetNamedArgument returns false when the argument is not present. - /// + /// TryGetNamedArgument returns false when the argument is not present. /// A task to monitor the async. [Test] public async Task WhenNamedArgumentAbsentThenTryGetReturnsFalse() @@ -52,24 +43,22 @@ public async Task WhenNamedArgumentAbsentThenTryGetReturnsFalse() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeTwo : Attribute { - public int Count { get; set; } + public int NamedValueTwo { get; set; } } - [MyAttr] + [AttributeTwo] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeTwo"); - var found = attribute.TryGetNamedArgument("Count", out int? value); + var found = attribute.TryGetNamedArgument("NamedValueTwo", out int? value); await Assert.That(found).IsFalse(); await Assert.That(value).IsNull(); } - /// - /// TryGetNamedArgument returns false and default when the argument name does not match. - /// + /// TryGetNamedArgument returns false and default when the argument name does not match. /// A task to monitor the async. [Test] public async Task WhenWrongArgumentNameThenTryGetReturnsFalse() @@ -78,14 +67,14 @@ public async Task WhenWrongArgumentNameThenTryGetReturnsFalse() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeThree : Attribute { - public int Count { get; set; } + public int NamedValueThree { get; set; } } - [MyAttr(Count = 5)] + [AttributeThree(NamedValueThree = 5)] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeThree"); var found = attribute.TryGetNamedArgument("Other", out int? value); @@ -93,9 +82,7 @@ public class C { } await Assert.That(value).IsNull(); } - /// - /// TryGetNamedArgument returns false when called on a null AttributeData. - /// + /// TryGetNamedArgument returns false when called on a null AttributeData. /// A task to monitor the async. [Test] public async Task WhenAttributeDataIsNullThenTryGetReturnsFalse() @@ -107,9 +94,7 @@ public async Task WhenAttributeDataIsNullThenTryGetReturnsFalse() await Assert.That(value).IsNull(); } - /// - /// TryGetNamedArgument retrieves a string named argument. - /// + /// TryGetNamedArgument retrieves a string named argument. /// A task to monitor the async. [Test] public async Task WhenStringNamedArgumentPresentThenTryGetReturnsValue() @@ -118,14 +103,14 @@ public async Task WhenStringNamedArgumentPresentThenTryGetReturnsValue() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeFour : Attribute { public string? Name { get; set; } } - [MyAttr(Name = "hello")] + [AttributeFour(Name = "hello")] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeFour"); var found = attribute.TryGetNamedArgument("Name", out string? value); @@ -133,9 +118,7 @@ public class C { } await Assert.That(value).IsEqualTo("hello"); } - /// - /// TryGetNamedArgument retrieves a bool named argument. - /// + /// TryGetNamedArgument retrieves a bool named argument. /// A task to monitor the async. [Test] public async Task WhenBoolNamedArgumentPresentThenTryGetReturnsValue() @@ -144,14 +127,14 @@ public async Task WhenBoolNamedArgumentPresentThenTryGetReturnsValue() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeFive : Attribute { public bool IsEnabled { get; set; } } - [MyAttr(IsEnabled = true)] + [AttributeFive(IsEnabled = true)] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeFive"); var found = attribute.TryGetNamedArgument("IsEnabled", out bool? value); @@ -159,9 +142,7 @@ public class C { } await Assert.That(value).IsTrue(); } - /// - /// GetNamedArgument returns the value when the argument is present. - /// + /// GetNamedArgument returns the value when the argument is present. /// A task to monitor the async. [Test] public async Task WhenNamedArgumentPresentThenGetNamedArgumentReturnsValue() @@ -170,23 +151,21 @@ public async Task WhenNamedArgumentPresentThenGetNamedArgumentReturnsValue() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeSix : Attribute { - public int Count { get; set; } + public int NamedValueSix { get; set; } } - [MyAttr(Count = 7)] + [AttributeSix(NamedValueSix = 1)] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeSix"); - var value = attribute.GetNamedArgument("Count"); + var value = attribute.GetNamedArgument("NamedValueSix"); - await Assert.That(value).IsEqualTo(7); + await Assert.That(value).IsEqualTo(1); } - /// - /// GetNamedArgument returns default when the argument is absent. - /// + /// GetNamedArgument returns default when the argument is absent. /// A task to monitor the async. [Test] public async Task WhenNamedArgumentAbsentThenGetNamedArgumentReturnsDefault() @@ -195,23 +174,21 @@ public async Task WhenNamedArgumentAbsentThenGetNamedArgumentReturnsDefault() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeSeven : Attribute { - public int Count { get; set; } + public int NamedValueSeven { get; set; } } - [MyAttr] + [AttributeSeven] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeSeven"); var value = attribute.GetNamedArgument("Count"); await Assert.That(value).IsEqualTo(0); } - /// - /// GetNamedArgument returns default when called on a null AttributeData. - /// + /// GetNamedArgument returns default when called on a null AttributeData. /// A task to monitor the async. [Test] public async Task WhenAttributeDataIsNullThenGetNamedArgumentReturnsDefault() @@ -222,9 +199,7 @@ public async Task WhenAttributeDataIsNullThenGetNamedArgumentReturnsDefault() await Assert.That(value).IsEqualTo(0); } - /// - /// GetConstructorArguments yields all string constructor arguments. - /// + /// GetConstructorArguments yields all string constructor arguments. /// A task to monitor the async. [Test] public async Task WhenStringConstructorArgsPresentThenGetConstructorArgumentsYieldsAll() @@ -233,25 +208,22 @@ public async Task WhenStringConstructorArgsPresentThenGetConstructorArgumentsYie using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute + public class AttributeEight : Attribute { - public MyAttr(string a, string b) { } + public AttributeEight(string a, string b) { } } - [MyAttr("hello", "world")] + [AttributeEight("hello", "world")] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeEight"); - var args = attribute.GetConstructorArguments().ToList(); + List args = [.. attribute.GetConstructorArguments()]; - await Assert.That(args.Count).IsEqualTo(2); await Assert.That(args[0]).IsEqualTo("hello"); await Assert.That(args[1]).IsEqualTo("world"); } - /// - /// GetConstructorArguments yields nothing when there are no constructor arguments of the requested type. - /// + /// GetConstructorArguments yields nothing when there are no constructor arguments of the requested type. /// A task to monitor the async. [Test] public async Task WhenNoMatchingConstructorArgsThenGetConstructorArgumentsIsEmpty() @@ -260,20 +232,18 @@ public async Task WhenNoMatchingConstructorArgsThenGetConstructorArgumentsIsEmpt using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute { } - [MyAttr] + public class AttributeNine : Attribute { } + [AttributeNine] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeNine"); - var args = attribute.GetConstructorArguments().ToList(); + List args = [.. attribute.GetConstructorArguments()]; await Assert.That(args.Count).IsEqualTo(0); } - /// - /// GetGenericType returns the type argument name for a generic attribute. - /// + /// GetGenericType returns the type argument name for a generic attribute. /// A task to monitor the async. [Test] public async Task WhenGenericAttributeThenGetGenericTypeReturnsTypeName() @@ -282,20 +252,18 @@ public async Task WhenGenericAttributeThenGetGenericTypeReturnsTypeName() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute { } - [MyAttr] + public class AttributeTen : Attribute { } + [AttributeTen] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeTen"); var type = attribute.GetGenericType(); await Assert.That(type).IsEqualTo("int"); } - /// - /// GetGenericType returns null for a non-generic attribute. - /// + /// GetGenericType returns null for a non-generic attribute. /// A task to monitor the async. [Test] public async Task WhenNonGenericAttributeThenGetGenericTypeReturnsNull() @@ -304,20 +272,18 @@ public async Task WhenNonGenericAttributeThenGetGenericTypeReturnsNull() using System; namespace T; [AttributeUsage(AttributeTargets.Class)] - public class MyAttr : Attribute { } - [MyAttr] + public class AttributeEleven : Attribute { } + [AttributeEleven] public class C { } """; - var attribute = GetAttribute(source, "T.C", "MyAttr"); + var attribute = GetAttribute(source, "T.C", "AttributeEleven"); var type = attribute.GetGenericType(); await Assert.That(type).IsNull(); } - /// - /// GetGenericType returns the type keyword for a generic argument using a built-in type. - /// + /// GetGenericType returns the type keyword for a generic argument using a built-in type. /// A task to monitor the async. [Test] public async Task WhenGenericAttributeWithClassTypeThenGetGenericTypeReturnsClassName() @@ -337,9 +303,7 @@ public class C { } await Assert.That(type).IsEqualTo("string"); } - /// - /// GatherForwardedAttributesFromClass collects non-trigger attributes from the class declaration. - /// + /// GatherForwardedAttributesFromClass collects non-trigger attributes from the class declaration. /// A task to monitor the async. [Test] public async Task WhenClassHasAttributesThenForwardedAttributesCollected() @@ -356,36 +320,83 @@ public class C { } """; var compilation = CreateCompilation(source); - var classDecl = compilation.SyntaxTrees - .SelectMany(t => t.GetRoot().DescendantNodes()) - .OfType() - .First(c => c.Identifier.Text == "C"); - - var semanticModel = compilation.GetSemanticModel(classDecl.SyntaxTree); + var classDeclaration = await GetClassDeclaration(compilation); + var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree); var typeSymbol = (INamedTypeSymbol)compilation.GetTypeByMetadataName("T.C")!; - var triggerAttr = typeSymbol.GetAttributes() - .First(a => a.AttributeClass?.Name == "TriggerAttr"); + AttributeData? triggerAttr = null; + foreach (var candidate in typeSymbol.GetAttributes()) + { + if (candidate.AttributeClass?.Name == "TriggerAttr") + { + triggerAttr = candidate; + break; + } + } - triggerAttr.GatherForwardedAttributesFromClass( + (triggerAttr ?? throw new InvalidOperationException("The trigger attribute was not found.")).GatherForwardedAttributesFromClass( semanticModel, - classDecl, + classDeclaration, default, out var forwarded); await Assert.That(forwarded.Length).IsGreaterThan(0); - await Assert.That(forwarded.Any(a => a.TypeName.Contains("TriggerAttr"))).IsFalse(); + var containsTriggerAttribute = false; + foreach (var forwardedAttribute in forwarded) + { + if (forwardedAttribute.TypeName.Contains("TriggerAttr", StringComparison.Ordinal)) + { + containsTriggerAttribute = true; + break; + } + } + + await Assert.That(containsTriggerAttribute).IsFalse(); + } + + /// Finds the class declaration used by the forwarded-attribute test. + /// The compilation containing the class declaration. + /// A task that resolves to the class declaration. + private static async Task GetClassDeclaration(CSharpCompilation compilation) + { + foreach (var syntaxTree in compilation.SyntaxTrees) + { + foreach (var node in (await syntaxTree.GetRootAsync()).DescendantNodes()) + { + if (node is ClassDeclarationSyntax { Identifier.Text: "C" } declaration) + { + return declaration; + } + } + } + + throw new InvalidOperationException("The test class declaration was not found."); } + /// Retrieves a named attribute from an in-memory test type. + /// The source text containing the target type. + /// The metadata name of the target type. + /// The simple name of the attribute to retrieve. + /// The requested attribute data. private static AttributeData GetAttribute(string source, string typeName, string attributeSimpleName) { var compilation = CreateCompilation(source); var typeSymbol = compilation.GetTypeByMetadataName(typeName) ?? throw new InvalidOperationException($"Type '{typeName}' not found in compilation."); - return typeSymbol.GetAttributes() - .First(a => a.AttributeClass?.Name == attributeSimpleName); + foreach (var attribute in typeSymbol.GetAttributes()) + { + if (attribute.AttributeClass?.Name == attributeSimpleName) + { + return attribute; + } + } + + throw new InvalidOperationException($"Attribute '{attributeSimpleName}' was not found."); } + /// Creates an in-memory compilation for an attribute extension test source. + /// The source text to compile. + /// The created compilation. private static CSharpCompilation CreateCompilation(string source) { var syntaxTree = CSharpSyntaxTree.ParseText( @@ -396,6 +407,6 @@ private static CSharpCompilation CreateCompilation(string source) assemblyName: "AttrDataExtTests", syntaxTrees: [syntaxTree], references: TestCompilationReferences.CreateDefault(), - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs index 50b16516..33a4588c 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// BindableDerivedListGeneratorTests. -/// +/// Tests bindable derived-list source generation. public class BindableDerivedListGeneratorTests : TestBase { - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task FromReactiveProperties() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/CodeFixerHelperTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/CodeFixerHelperTests.cs new file mode 100644 index 00000000..73285e85 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/CodeFixerHelperTests.cs @@ -0,0 +1,273 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using CodeFixerHelpers = ReactiveUI.SourceGenerators.CodeFixers.Helpers; + +namespace ReactiveUI.SourceGenerator.Tests; + +/// Tests the helper implementations used by the code-fix assembly. +public sealed class CodeFixerHelperTests +{ + /// The first test value. + private const int FirstValue = 1; + + /// The second test value. + private const int SecondValue = 2; + + /// The third test value. + private const int ThirdValue = 3; + + /// The fourth test value. + private const int FourthValue = 4; + + /// The fifth test value. + private const int FifthValue = 5; + + /// The number of values that forces the builder to grow. + private const int GrowthCount = 16; + + /// Equivalent hash states compare equally through typed and object equality. + /// A task to monitor the async. + [Test] + public async Task HashCode_WithEquivalentValues_UsesValueEquality() + { + var first = CreateHash(FirstValue, SecondValue, ThirdValue, FourthValue, FifthValue); + var second = CreateHash(FirstValue, SecondValue, ThirdValue, FourthValue, FifthValue); + object boxedSecond = second; + IEquatable equatable = first; + + await Assert.That(first.Equals(second)).IsTrue(); + await Assert.That(equatable.Equals(second)).IsTrue(); + await Assert.That(first.Equals(boxedSecond)).IsTrue(); + await Assert.That(first.Equals(null)).IsFalse(); + await Assert.That(first.GetHashCode()).IsEqualTo(second.GetHashCode()); + await Assert.That(first.ToHashCode()).IsEqualTo(second.ToHashCode()); + } + + /// Hash states retain value order and include null values consistently. + /// A task to monitor the async. + [Test] + public async Task HashCode_WithDifferentOrderOrNull_TracksTheAddedValues() + { + var ordered = CreateHash(FirstValue, SecondValue, ThirdValue, FourthValue); + var reordered = CreateHash(FourthValue, ThirdValue, SecondValue, FirstValue); + var withNull = default(CodeFixerHelpers.HashCode); + var withZero = default(CodeFixerHelpers.HashCode); + + withNull.Add(null); + withZero.Add(0); + + await Assert.That(ordered.Equals(reordered)).IsFalse(); + await Assert.That(ordered.ToHashCode()).IsEqualTo(CreateHash(FirstValue, SecondValue, ThirdValue, FourthValue).ToHashCode()); + await Assert.That(withNull.Equals(withZero)).IsTrue(); + await Assert.That(withNull.ToHashCode()).IsEqualTo(withZero.ToHashCode()); + } + + /// Equatable arrays use ordered element value equality for empty, single, and multiple values. + /// A task to monitor the async. + [Test] + public async Task EquatableArray_WithEmptySingleAndMultipleValues_UsesOrderedValueEquality() + { + var empty = CodeFixerHelpers.EquatableArray.FromImmutableArray(ImmutableArray.Empty); + var single = CodeFixerHelpers.EquatableArray.FromImmutableArray([FirstValue]); + var multiple = CodeFixerHelpers.EquatableArray.FromImmutableArray([FirstValue, SecondValue, ThirdValue]); + var reordered = CodeFixerHelpers.EquatableArray.FromImmutableArray([ThirdValue, SecondValue, FirstValue]); + + await Assert.That(empty.IsEmpty).IsTrue(); + await Assert.That(empty.Equals(CodeFixerHelpers.EquatableArray.FromImmutableArray(ImmutableArray.Empty))).IsTrue(); + await Assert.That(single[0]).IsEqualTo(FirstValue); + await Assert.That(multiple.Equals(reordered)).IsFalse(); + await Assert.That(multiple.ToArray().SequenceEqual([FirstValue, SecondValue, ThirdValue])).IsTrue(); + } + + /// Equatable arrays support object equality, null, conversion, enumeration, and stable hashes for equal values. + /// A task to monitor the async. + [Test] + public async Task EquatableArray_WithEquivalentObject_RoundTripsAndHasEqualHashCode() + { + var source = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue); + var array = CodeFixerHelpers.EquatableArray.FromImmutableArray(source); + CodeFixerHelpers.EquatableArray implicitlyConverted = source; + var defaultArray = default(CodeFixerHelpers.EquatableArray); + object equivalent = CodeFixerHelpers.EquatableArray.FromImmutableArray(source); + IEquatable> equatable = array; + ImmutableArray roundTripped = array; + IEnumerable enumerable = array; + var directEnumerator = array.GetEnumerator(); + var directlyEnumerated = new List(); + while (directEnumerator.MoveNext()) + { + directlyEnumerated.Add(directEnumerator.Current); + } + + var nonGenericEnumerable = (System.Collections.IEnumerable)array; + var nonGenericValues = CollectNonGenericValues(nonGenericEnumerable); + + await Assert.That(array.Equals(equivalent)).IsTrue(); + await Assert.That(array.Equals(null)).IsFalse(); + await Assert.That(equatable.Equals(array)).IsTrue(); + await Assert.That(implicitlyConverted == array).IsTrue(); + await Assert.That(implicitlyConverted != array).IsFalse(); + await Assert.That(defaultArray.GetHashCode()).IsEqualTo(0); + await Assert.That(array.GetHashCode()).IsEqualTo(((CodeFixerHelpers.EquatableArray)equivalent).GetHashCode()); + await Assert.That(roundTripped.SequenceEqual(source)).IsTrue(); + await Assert.That(enumerable.SequenceEqual(source)).IsTrue(); + await Assert.That(directlyEnumerated.SequenceEqual(source)).IsTrue(); + await Assert.That(nonGenericValues.SequenceEqual(source)).IsTrue(); + } + + /// The extension creates the same value as the factory. + /// A task to monitor the async. + [Test] + public async Task EquatableArrayExtension_WithImmutableArray_MatchesFactory() + { + var source = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue); + var fromExtension = CodeFixerHelpers.EquatableArrayExtensions.AsEquatableArray(source); + var fromFactory = CodeFixerHelpers.EquatableArray.FromImmutableArray(source); + + await Assert.That(fromExtension.Equals(fromFactory)).IsTrue(); + } + + /// A builder handles empty, single, growing, range, copy, and enumeration paths. + /// A task to monitor the async. + [Test] + public async Task ImmutableArrayBuilder_WithValuesAndGrowth_PreservesContents() + { + ImmutableArray empty; + ImmutableArray values; + int[] copy; + int count; + int writtenLength; + int[] enumeratedValues; + int[] copiedValues; + int[] nonGenericValues; + bool isReadOnly; + int collectionCount; + ICollection collection; + using (var builder = CodeFixerHelpers.ImmutableArrayBuilder.Rent()) + { + empty = builder.ToImmutable(); + builder.Add(FirstValue); + builder.AddRange([SecondValue, ThirdValue]); + for (var value = FourthValue; value <= GrowthCount; value++) + { + builder.Add(value); + } + + values = builder.ToImmutable(); + copy = builder.ToArray(); + count = builder.Count; + writtenLength = builder.WrittenSpan.Length; + enumeratedValues = [.. builder.AsEnumerable()]; + collection = (ICollection)builder.AsEnumerable(); + collection.Add(GrowthCount + FirstValue); + copiedValues = new int[collection.Count]; + collection.CopyTo(copiedValues, 0); + nonGenericValues = CollectNonGenericValues((System.Collections.IEnumerable)builder.AsEnumerable()); + isReadOnly = collection.IsReadOnly; + collectionCount = collection.Count; + } + + await Assert.That(empty.IsEmpty).IsTrue(); + await Assert.That(count).IsEqualTo(GrowthCount); + await Assert.That(writtenLength).IsEqualTo(GrowthCount); + await Assert.That(values.SequenceEqual(Enumerable.Range(FirstValue, GrowthCount))).IsTrue(); + await Assert.That(copy.SequenceEqual(values)).IsTrue(); + await Assert.That(enumeratedValues.SequenceEqual(values)).IsTrue(); + await Assert.That(copiedValues.SequenceEqual(Enumerable.Range(FirstValue, GrowthCount + FirstValue))).IsTrue(); + await Assert.That(nonGenericValues.SequenceEqual(copiedValues)).IsTrue(); + await Assert.That(isReadOnly).IsTrue(); + await Assert.That(collectionCount).IsEqualTo(GrowthCount + FirstValue); + await Assert.That(collection.Clear).Throws(); + await Assert.That(() => collection.Contains(FirstValue)).Throws(); + await Assert.That(() => collection.Remove(FirstValue)).Throws(); + } + + /// A character builder returns its written text and supports interface disposal. + /// A task to monitor the async. + [Test] + public async Task ImmutableArrayBuilder_WithCharacters_FormatsAndDisposesThroughInterface() + { + const string expected = "coverage"; + string actual; + IDisposable disposable; + var builder = CodeFixerHelpers.ImmutableArrayBuilder.Rent(); + builder.AddRange(expected.AsSpan()); + actual = builder.ToString(); + disposable = (IDisposable)builder.AsEnumerable(); + disposable.Dispose(); + disposable.Dispose(); + builder.Dispose(); + + await Assert.That(actual).IsEqualTo(expected); + } + + /// Disposed builders and the unsupported collection operations report their intended failures. + /// A task to monitor the async. + [Test] + public async Task ImmutableArrayBuilder_AfterDisposeAndUnsupportedCollectionCalls_ReportsErrors() + { + await Assert.That(DisposeBuilderTwice()).IsTrue(); + await Assert.That(AccessDisposedBuilder).Throws(); + await Assert.That(UnsupportedCollectionOperation).Throws(); + } + + /// Creates a hash from the provided values. + /// The values to add. + /// The resulting hash state. + private static CodeFixerHelpers.HashCode CreateHash(params int[] values) + { + var hash = default(CodeFixerHelpers.HashCode); + foreach (var value in values) + { + hash.Add(value); + } + + return hash; + } + + /// Copies integer values from a non-generic enumerable without LINQ. + /// The source values. + /// The copied integer values. + private static int[] CollectNonGenericValues(System.Collections.IEnumerable source) + { + List values = []; + foreach (var value in source) + { + if (value is int integer) + { + values.Add(integer); + } + } + + return [.. values]; + } + + /// Accesses a disposed builder. + private static void AccessDisposedBuilder() + { + var builder = CodeFixerHelpers.ImmutableArrayBuilder.Rent(); + builder.Dispose(); + _ = builder.Count; + } + + /// Disposes a builder twice. + /// after both dispose calls complete. + private static bool DisposeBuilderTwice() + { + var builder = CodeFixerHelpers.ImmutableArrayBuilder.Rent(); + builder.Dispose(); + builder.Dispose(); + return true; + } + + /// Invokes an unsupported operation exposed by a builder enumeration. + private static void UnsupportedCollectionOperation() + { + using var builder = CodeFixerHelpers.ImmutableArrayBuilder.Rent(); + ICollection collection = (ICollection)builder.AsEnumerable(); + _ = collection.Contains(FirstValue); + } +} diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs index 370704dc..bbb12aac 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for the BindableDerivedList generator covering edge cases. -/// +/// Extended unit tests for the BindableDerivedList generator covering edge cases. public class DerivedListExtTests : TestBase { - /// - /// Tests BindableDerivedList with multiple lists. - /// + /// Tests BindableDerivedList with multiple lists. /// A task to monitor the async. [Test] public Task MultipleLists() @@ -39,9 +34,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with complex generic type. - /// + /// Tests BindableDerivedList with complex generic type. /// A task to monitor the async. [Test] public Task ComplexGeneric() @@ -68,9 +61,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList in nested class. - /// + /// Tests BindableDerivedList in nested class. /// A task to monitor the async. [Test] public Task NestedClass() @@ -103,9 +94,7 @@ public partial class DeepInnerVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with generic class. - /// + /// Tests BindableDerivedList with generic class. /// A task to monitor the async. [Test] public Task GenericClass() @@ -126,9 +115,7 @@ public partial class GenericVM where T : class return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with record type. - /// + /// Tests BindableDerivedList with record type. /// A task to monitor the async. [Test] public Task RecordClass() @@ -152,9 +139,7 @@ public partial record TestVMRecord return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with nullable reference type elements. - /// + /// Tests BindableDerivedList with nullable reference type elements. /// A task to monitor the async. [Test] public Task NullableElements() @@ -175,9 +160,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with interface element type. - /// + /// Tests BindableDerivedList with interface element type. /// A task to monitor the async. [Test] public Task InterfaceType() @@ -208,9 +191,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with tuple type elements. - /// + /// Tests BindableDerivedList with tuple type elements. /// A task to monitor the async. [Test] public Task TupleType() @@ -231,9 +212,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList in different namespaces. - /// + /// Tests BindableDerivedList in different namespaces. /// A task to monitor the async. [Test] public Task DiffNamespaces() @@ -264,9 +243,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with enum element type. - /// + /// Tests BindableDerivedList with enum element type. /// A task to monitor the async. [Test] public Task EnumType() @@ -289,9 +266,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with struct element type. - /// + /// Tests BindableDerivedList with struct element type. /// A task to monitor the async. [Test] public Task StructType() @@ -318,9 +293,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with Guid element type. - /// + /// Tests BindableDerivedList with Guid element type. /// A task to monitor the async. [Test] public Task GuidType() @@ -342,9 +315,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with DateTime element type. - /// + /// Tests BindableDerivedList with DateTime element type. /// A task to monitor the async. [Test] public Task DateTimeType() @@ -369,9 +340,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList combined with reactive properties. - /// + /// Tests BindableDerivedList combined with reactive properties. /// A task to monitor the async. [Test] public Task WithReactive() @@ -403,9 +372,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList always generates public properties even for internal classes. - /// + /// Tests BindableDerivedList always generates public properties even for internal classes. /// A task to monitor the async. [Test] public Task InternalClassGeneratesPublicProperty() @@ -427,9 +394,7 @@ internal partial class ViewModel : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests BindableDerivedList with all supported property access modifiers. - /// + /// Tests BindableDerivedList with all supported property access modifiers. /// A task to monitor the async. [Test] public Task AccessModifiers() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DiagnosticSuppressorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DiagnosticSuppressorTests.cs new file mode 100644 index 00000000..b6ce76e7 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DiagnosticSuppressorTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.Diagnostics; +using ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +namespace ReactiveUI.SourceGenerator.Tests; + +/// Tests the diagnostic suppression contracts exposed by the generators. +public sealed class DiagnosticSuppressorTests +{ + /// The compiler diagnostic for an invalid attribute target. + private const string InvalidAttributeTargetDiagnosticId = "CS0657"; + + /// The number of invalid target diagnostics exercised together. + private const int InvalidTargetDiagnosticCount = 3; + + /// Source containing each invalid generated-member attribute target. + private const string AttributeTargetSource = """ + using System; + + namespace ReactiveUI.SourceGenerators + { + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class ReactiveAttribute : Attribute; + + [AttributeUsage(AttributeTargets.Method)] + public sealed class ReactiveCommandAttribute : Attribute; + } + + namespace TestNs + { + public partial class ViewModel + { + [ReactiveUI.SourceGenerators.Reactive] + [field: Obsolete] + public partial string Name { get; set; } + + [ReactiveUI.SourceGenerators.Reactive] + [property: Obsolete] + private string _name = string.Empty; + + [ReactiveUI.SourceGenerators.ReactiveCommand] + [field: Obsolete] + partial void Save(); + } + } + """; + + /// Each suppressor exposes the intended diagnostic-to-suppression mapping. + /// A task to monitor the async. + [Test] + public async Task SupportedSuppressions_ExposeExpectedDescriptor() + { + await AssertDescriptor( + new ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor(), + "RXUISPR0001", + InvalidAttributeTargetDiagnosticId); + await AssertDescriptor( + new ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor(), + "RXUISPR0002", + "IDE0052"); + await AssertDescriptor( + new ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor(), + "RXUISPR0003", + "CA1822"); + await AssertDescriptor( + new OAPHMethodDoesNotNeedToBeStaticDiagnosticSuppressor(), + "RXUISPR0003", + "CA1822"); + await AssertDescriptor( + new ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor(), + "RXUISPR0004", + "RCS1169"); + await AssertDescriptor( + new ReactiveAttributeWithFieldTargetDiagnosticSuppressor(), + "RXUISPR0005", + InvalidAttributeTargetDiagnosticId); + await AssertDescriptor( + new ReactiveAttributeWithPropertyTargetDiagnosticSuppressor(), + "RXUISPR0005", + InvalidAttributeTargetDiagnosticId); + } + + /// Reactive field, property, and command targets suppress matching compiler diagnostics. + /// A task to monitor the async. + [Test] + public async Task AttributeTargetSuppressors_SuppressMatchingCompilerDiagnostics() + { + var syntaxTree = CSharpSyntaxTree.ParseText( + AttributeTargetSource, + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview)); + var compilation = CSharpCompilation.Create( + nameof(AttributeTargetSuppressors_SuppressMatchingCompilerDiagnostics), + [syntaxTree], + TestCompilationReferences.CreateDefault(), + new(OutputKind.DynamicallyLinkedLibrary)); + ImmutableArray suppressors = + [ + new ReactiveAttributeWithFieldTargetDiagnosticSuppressor(), + new ReactiveAttributeWithPropertyTargetDiagnosticSuppressor(), + new ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor(), + ]; + var options = new CompilationWithAnalyzersOptions( + new AnalyzerOptions(ImmutableArray.Empty), + onAnalyzerException: null, + concurrentAnalysis: true, + logAnalyzerExecutionTime: false, + reportSuppressedDiagnostics: true); + + var diagnostics = await compilation + .WithAnalyzers(suppressors, options) + .GetAllDiagnosticsAsync(); + var suppressedDiagnosticCount = 0; + + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Id == InvalidAttributeTargetDiagnosticId && diagnostic.IsSuppressed) + { + suppressedDiagnosticCount++; + } + } + + await Assert.That(suppressedDiagnosticCount).IsEqualTo(InvalidTargetDiagnosticCount); + } + + /// Asserts a suppressor's single supported descriptor. + /// The suppressor under test. + /// The expected suppression identifier. + /// The expected suppressed diagnostic identifier. + /// A task to monitor the async. + private static async Task AssertDescriptor( + DiagnosticSuppressor suppressor, + string suppressionId, + string diagnosticId) + { + var descriptor = suppressor.SupportedSuppressions.Single(); + + await Assert.That(descriptor.Id).IsEqualTo(suppressionId); + await Assert.That(descriptor.SuppressedDiagnosticId).IsEqualTo(diagnosticId); + await Assert.That(descriptor.Justification.ToString()).IsNotEmpty(); + } +} diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/EquatableArrayTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/EquatableArrayTests.cs index 01869a21..c2ef8272 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/EquatableArrayTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/EquatableArrayTests.cs @@ -1,62 +1,102 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for . -/// +/// Unit tests for . public sealed class EquatableArrayTests { - /// - /// Two arrays with identical elements are equal. - /// + /// The first element index. + private const int FirstIndex = 0; + + /// The second element index. + private const int SecondIndex = 1; + + /// The third element index. + private const int ThirdIndex = 2; + + /// The expected number of elements in a two-element array. + private const int TwoElements = 2; + + /// The expected number of elements in a three-element array. + private const int ThreeElements = 3; + + /// The first value used by the tests. + private const int FirstValue = 1; + + /// The second value used by the tests. + private const int SecondValue = 2; + + /// The third value used by the tests. + private const int ThirdValue = 3; + + /// The value used to make an array unequal. + private const int DifferentValue = 4; + + /// The fifth value used by the tests. + private const int FifthValue = 5; + + /// The sixth value used by the tests. + private const int SixthValue = 6; + + /// The seventh value used by the tests. + private const int SeventhValue = 7; + + /// The eighth value used by the tests. + private const int EighthValue = 8; + + /// The ninth value used by the tests. + private const int NinthValue = 9; + + /// The tenth value used by the tests. + private const int TenthValue = 10; + + /// The twentieth value used by the tests. + private const int TwentiethValue = 20; + + /// The thirtieth value used by the tests. + private const int ThirtiethValue = 30; + + /// Two arrays with identical elements are equal. /// A task to monitor the async. [Test] public async Task WhenSameElementsThenEqual() { - var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); - var b = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); + var a = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); + var b = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); await Assert.That(a == b).IsTrue(); await Assert.That(a.Equals(b)).IsTrue(); await Assert.That(a != b).IsFalse(); } - /// - /// Two arrays with different elements are not equal. - /// + /// Two arrays with different elements are not equal. /// A task to monitor the async. [Test] public async Task WhenDifferentElementsThenNotEqual() { - var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); - var b = ImmutableArray.Create(1, 2, 4).AsEquatableArray(); + var a = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); + var b = ImmutableArray.Create(FirstValue, SecondValue, DifferentValue).AsEquatableArray(); await Assert.That(a == b).IsFalse(); await Assert.That(a != b).IsTrue(); } - /// - /// Arrays with the same elements in different order are not equal. - /// + /// Arrays with the same elements in different order are not equal. /// A task to monitor the async. [Test] public async Task WhenDifferentOrderThenNotEqual() { - var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); - var b = ImmutableArray.Create(3, 2, 1).AsEquatableArray(); + var a = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); + var b = ImmutableArray.Create(ThirdValue, SecondValue, FirstValue).AsEquatableArray(); await Assert.That(a == b).IsFalse(); } - /// - /// An empty array equals another empty array. - /// + /// An empty array equals another empty array. /// A task to monitor the async. [Test] public async Task WhenBothEmptyThenEqual() @@ -68,159 +108,128 @@ public async Task WhenBothEmptyThenEqual() await Assert.That(a.IsEmpty).IsTrue(); } - /// - /// An empty array is not equal to a non-empty array. - /// + /// An empty array is not equal to a non-empty array. /// A task to monitor the async. [Test] public async Task WhenOneEmptyThenNotEqual() { var a = ImmutableArray.Empty.AsEquatableArray(); - var b = ImmutableArray.Create(1).AsEquatableArray(); + var b = ImmutableArray.Create(FirstValue).AsEquatableArray(); await Assert.That(a == b).IsFalse(); } - /// - /// The indexer returns the element at the given position. - /// + /// The indexer returns the element at the given position. /// A task to monitor the async. [Test] public async Task WhenIndexedThenReturnsCorrectElement() { - var arr = ImmutableArray.Create(10, 20, 30).AsEquatableArray(); + var arr = ImmutableArray.Create(TenthValue, TwentiethValue, ThirtiethValue).AsEquatableArray(); - await Assert.That(arr[0]).IsEqualTo(10); - await Assert.That(arr[1]).IsEqualTo(20); - await Assert.That(arr[2]).IsEqualTo(30); + await Assert.That(arr[FirstIndex]).IsEqualTo(TenthValue); + await Assert.That(arr[SecondIndex]).IsEqualTo(TwentiethValue); + await Assert.That(arr[ThirdIndex]).IsEqualTo(ThirtiethValue); } - /// - /// Enumeration yields all elements in order. - /// + /// Enumeration yields all elements in order. /// A task to monitor the async. [Test] public async Task WhenEnumeratedThenYieldsAllElements() { - var expected = new[] { 1, 2, 3 }; + var expected = new[] { FirstValue, SecondValue, ThirdValue }; var arr = ImmutableArray.Create(expected).AsEquatableArray(); + var actual = new List(arr); - var actual = arr.ToList(); - - await Assert.That(actual.Count).IsEqualTo(3); - await Assert.That(actual[0]).IsEqualTo(1); - await Assert.That(actual[1]).IsEqualTo(2); - await Assert.That(actual[2]).IsEqualTo(3); + await Assert.That(actual.Count).IsEqualTo(ThreeElements); + await Assert.That(actual[FirstIndex]).IsEqualTo(FirstValue); + await Assert.That(actual[SecondIndex]).IsEqualTo(SecondValue); + await Assert.That(actual[ThirdIndex]).IsEqualTo(ThirdValue); } - /// - /// Implicit conversion from ImmutableArray preserves elements. - /// + /// Implicit conversion from ImmutableArray preserves elements. /// A task to monitor the async. [Test] public async Task WhenImplicitlyConvertedFromImmutableArrayThenPreservesElements() { - var immutable = ImmutableArray.Create(5, 6, 7); - EquatableArray equatable = immutable; + EquatableArray equatable = ImmutableArray.Create(FifthValue, SixthValue, SeventhValue); - await Assert.That(equatable[0]).IsEqualTo(5); - await Assert.That(equatable[1]).IsEqualTo(6); - await Assert.That(equatable[2]).IsEqualTo(7); + await Assert.That(equatable[FirstIndex]).IsEqualTo(FifthValue); + await Assert.That(equatable[SecondIndex]).IsEqualTo(SixthValue); + await Assert.That(equatable[ThirdIndex]).IsEqualTo(SeventhValue); } - /// - /// Implicit conversion to ImmutableArray preserves elements. - /// + /// Implicit conversion to ImmutableArray preserves elements. /// A task to monitor the async. [Test] public async Task WhenImplicitlyConvertedToImmutableArrayThenPreservesElements() { - var equatable = ImmutableArray.Create(8, 9).AsEquatableArray(); - ImmutableArray immutable = equatable; + ImmutableArray immutable = ImmutableArray.Create(EighthValue, NinthValue).AsEquatableArray(); - await Assert.That(immutable.Length).IsEqualTo(2); - await Assert.That(immutable[0]).IsEqualTo(8); - await Assert.That(immutable[1]).IsEqualTo(9); + await Assert.That(immutable.Length).IsEqualTo(TwoElements); + await Assert.That(immutable[FirstIndex]).IsEqualTo(EighthValue); + await Assert.That(immutable[SecondIndex]).IsEqualTo(NinthValue); } - /// - /// ToArray returns a mutable copy with the same elements. - /// + /// ToArray returns a mutable copy with the same elements. /// A task to monitor the async. [Test] public async Task WhenToArrayCalledThenReturnsMutableCopy() { - var arr = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); + var arr = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); var copy = arr.ToArray(); - await Assert.That(copy.Length).IsEqualTo(3); - await Assert.That(copy[0]).IsEqualTo(1); - await Assert.That(copy[2]).IsEqualTo(3); + await Assert.That(copy.Length).IsEqualTo(ThreeElements); + await Assert.That(copy[FirstIndex]).IsEqualTo(FirstValue); + await Assert.That(copy[ThirdIndex]).IsEqualTo(ThirdValue); } - /// - /// AsSpan returns a span over the elements. - /// + /// AsSpan returns a span over the elements. + /// A task to monitor the async. [Test] - public void WhenAsSpanCalledThenSpanCoversElements() + public async Task WhenAsSpanCalledThenSpanCoversElements() { - var arr = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); + var arr = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); var span = arr.AsSpan(); var length = span.Length; - var mid = span[1]; - - if (length != 3) - { - throw new InvalidOperationException($"Expected span length 3, got {length}."); - } + var middle = span[SecondIndex]; - if (mid != 2) - { - throw new InvalidOperationException($"Expected span[1] == 2, got {mid}."); - } + await Assert.That(length).IsEqualTo(ThreeElements); + await Assert.That(middle).IsEqualTo(SecondValue); } - /// - /// GetHashCode returns the same value for equal arrays. - /// + /// GetHashCode returns the same value for equal arrays. /// A task to monitor the async. [Test] public async Task WhenEqualArraysThenSameHashCode() { - var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); - var b = ImmutableArray.Create(1, 2, 3).AsEquatableArray(); + var a = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); + var b = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue).AsEquatableArray(); await Assert.That(a.GetHashCode()).IsEqualTo(b.GetHashCode()); } - /// - /// Equals(object) returns true when passed an equal EquatableArray. - /// + /// Equals(object) returns true when passed an equal EquatableArray. /// A task to monitor the async. [Test] public async Task WhenEqualsObjectCalledWithEqualArrayThenReturnsTrue() { - var a = ImmutableArray.Create(1, 2).AsEquatableArray(); - object b = ImmutableArray.Create(1, 2).AsEquatableArray(); + var a = ImmutableArray.Create(FirstValue, SecondValue).AsEquatableArray(); + object b = ImmutableArray.Create(FirstValue, SecondValue).AsEquatableArray(); await Assert.That(a.Equals(b)).IsTrue(); } - /// - /// Equals(object) returns false when passed null. - /// + /// Equals(object) returns false when passed null. /// A task to monitor the async. [Test] public async Task WhenEqualsObjectCalledWithNullThenReturnsFalse() { - var a = ImmutableArray.Create(1).AsEquatableArray(); + var a = ImmutableArray.Create(FirstValue).AsEquatableArray(); await Assert.That(a.Equals(null)).IsFalse(); } - /// - /// AsImmutableArray round-trips back to ImmutableArray. - /// + /// AsImmutableArray round-trips back to ImmutableArray. /// A task to monitor the async. [Test] public async Task WhenAsImmutableArrayCalledThenRoundTrips() @@ -232,33 +241,29 @@ public async Task WhenAsImmutableArrayCalledThenRoundTrips() await Assert.That(roundTripped.SequenceEqual(source)).IsTrue(); } - /// - /// FromImmutableArray static factory produces an equal instance to the extension method. - /// + /// FromImmutableArray static factory produces an equal instance to the extension method. /// A task to monitor the async. [Test] public async Task WhenCreatedViaFactoryThenEqualsExtensionMethod() { - var immutable = ImmutableArray.Create(1, 2, 3); + var immutable = ImmutableArray.Create(FirstValue, SecondValue, ThirdValue); var viaExtension = immutable.AsEquatableArray(); var viaFactory = EquatableArray.FromImmutableArray(immutable); await Assert.That(viaExtension == viaFactory).IsTrue(); } - /// - /// IEnumerable<T> explicit interface yields elements correctly. - /// + /// IEnumerable<T> explicit interface yields elements correctly. /// A task to monitor the async. [Test] public async Task WhenEnumeratedAsIEnumerableThenYieldsCorrectly() { - IEnumerable arr = ImmutableArray.Create(7, 8, 9).AsEquatableArray(); - var list = arr.ToList(); + IEnumerable arr = ImmutableArray.Create(SeventhValue, EighthValue, NinthValue).AsEquatableArray(); + var list = new List(arr); - await Assert.That(list.Count).IsEqualTo(3); - await Assert.That(list[0]).IsEqualTo(7); - await Assert.That(list[1]).IsEqualTo(8); - await Assert.That(list[2]).IsEqualTo(9); + await Assert.That(list.Count).IsEqualTo(ThreeElements); + await Assert.That(list[FirstIndex]).IsEqualTo(SeventhValue); + await Assert.That(list[SecondIndex]).IsEqualTo(EighthValue); + await Assert.That(list[ThirdIndex]).IsEqualTo(NinthValue); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/FieldSyntaxExtensionTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/FieldSyntaxExtensionTests.cs index bc31ed9d..c8a1dcdc 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/FieldSyntaxExtensionTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/FieldSyntaxExtensionTests.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Extensions; @@ -15,10 +14,7 @@ namespace ReactiveUI.SourceGenerator.Tests; /// public sealed class FieldSyntaxExtensionTests { - /// - /// A field named with leading underscore prefix has the underscore stripped - /// and the first letter upper-cased. - /// + /// A field named with leading underscore prefix has the underscore stripped and the first letter upper-cased. /// A task to monitor the async. [Test] public async Task WhenFieldHasLeadingUnderscoreThenPropertyNameCapitalises() @@ -37,10 +33,7 @@ public class C await Assert.That(name).IsEqualTo("MyField"); } - /// - /// A field named with "m_" prefix has the prefix stripped and the first remaining - /// letter upper-cased. - /// + /// A field named with "m_" prefix has the prefix stripped and the first remaining letter upper-cased. /// A task to monitor the async. [Test] public async Task WhenFieldHasMUnderscorePrefixThenPropertyNameCapitalises() @@ -59,9 +52,7 @@ public class C await Assert.That(name).IsEqualTo("Value"); } - /// - /// A field named with lowerCamel (no prefix) has its first letter upper-cased. - /// + /// A field named with lowerCamel (no prefix) has its first letter upper-cased. /// A task to monitor the async. [Test] public async Task WhenFieldHasLowerCamelThenPropertyNameCapitalises() @@ -80,9 +71,7 @@ public class C await Assert.That(name).IsEqualTo("Count"); } - /// - /// Multiple leading underscores are all stripped before capitalisation. - /// + /// Multiple leading underscores are all stripped before capitalisation. /// A task to monitor the async. [Test] public async Task WhenFieldHasMultipleLeadingUnderscoresThenAllStripped() @@ -101,9 +90,7 @@ public class C await Assert.That(name).IsEqualTo("Item"); } - /// - /// A single-character field name (after stripping) still capitalises correctly. - /// + /// A single-character field name (after stripping) still capitalises correctly. /// A task to monitor the async. [Test] public async Task WhenFieldIsOneCharacterThenCapitalisedCorrectly() @@ -122,9 +109,7 @@ public class C await Assert.That(name).IsEqualTo("X"); } - /// - /// A property named "MyProperty" produces a backing field named "_myProperty". - /// + /// A property named "MyProperty" produces a backing field named "_myProperty". /// A task to monitor the async. [Test] public async Task WhenPropertyNamedMyPropertyThenFieldIsUnderscoreLower() @@ -143,9 +128,7 @@ public class C await Assert.That(name).IsEqualTo("_myProperty"); } - /// - /// A single-character property name produces a correct field name. - /// + /// A single-character property name produces a correct field name. /// A task to monitor the async. [Test] public async Task WhenPropertyIsSingleCharacterThenFieldNameIsCorrect() @@ -164,9 +147,7 @@ public class C await Assert.That(name).IsEqualTo("_x"); } - /// - /// A property already starting with a lowercase letter still prefixes underscore. - /// + /// A property already starting with a lowercase letter still prefixes underscore. /// A task to monitor the async. [Test] public async Task WhenPropertyStartsWithLowercaseThenFieldNameHasUnderscore() @@ -185,22 +166,45 @@ public class C await Assert.That(name).IsEqualTo("_value"); } + /// Finds the single field symbol with the requested name. + /// The source text containing the field. + /// The field name to find. + /// The matching field symbol. private static IFieldSymbol GetFieldSymbol(string source, string fieldName) { var compilation = CreateCompilation(source); - return compilation.GetSymbolsWithName(fieldName, SymbolFilter.Member) - .OfType() - .Single(); + foreach (var symbol in compilation.GetSymbolsWithName(fieldName, SymbolFilter.Member)) + { + if (symbol is IFieldSymbol fieldSymbol) + { + return fieldSymbol; + } + } + + throw new InvalidOperationException($"Field '{fieldName}' was not found."); } + /// Finds the single property symbol with the requested name. + /// The source text containing the property. + /// The property name to find. + /// The matching property symbol. private static IPropertySymbol GetPropertySymbol(string source, string propertyName) { var compilation = CreateCompilation(source); - return compilation.GetSymbolsWithName(propertyName, SymbolFilter.Member) - .OfType() - .Single(); + foreach (var symbol in compilation.GetSymbolsWithName(propertyName, SymbolFilter.Member)) + { + if (symbol is IPropertySymbol propertySymbol) + { + return propertySymbol; + } + } + + throw new InvalidOperationException($"Property '{propertyName}' was not found."); } + /// Creates an in-memory compilation for a field or property test source. + /// The source text to compile. + /// The created compilation. private static CSharpCompilation CreateCompilation(string source) { var syntaxTree = CSharpSyntaxTree.ParseText( @@ -211,6 +215,6 @@ private static CSharpCompilation CreateCompilation(string source) assemblyName: "FieldSyntaxTests", syntaxTrees: [syntaxTree], references: TestCompilationReferences.CreateDefault(), - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs index 63080bbc..800e80af 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs @@ -1,18 +1,22 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Text; +using Microsoft.CodeAnalysis.Text; + namespace ReactiveUI.SourceGenerator.Tests; -/// -/// IViewForGeneratorTests. -/// +/// Tests view-for source generation. public class IViewForGeneratorTests : TestBase { - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// The generated source count for four supported platform targets. + private const int SupportedPlatformGeneratedSourceCount = 6; + + /// The generated source count when no target can produce a view implementation. + private const int NoViewImplementationGeneratedSourceCount = 2; + + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task Basic() @@ -52,4 +56,118 @@ public partial class TestViewModel : ReactiveObject // Act: Initialize the helper and run the generator. Assert: Verify the generated code. return TestHelper.TestPass(sourceCode); } + + /// Verifies the platform-specific IViewFor source shapes without creating snapshots. + /// A task representing the asynchronous assertion work. + [Test] + public async Task GeneratesPlatformSpecificSourcesForSupportedViewBases() + { + var generatedSources = RunIViewForGenerator( + """ + using ReactiveUI.SourceGenerators; + + namespace System.Windows { public class Window { } } + namespace System.Windows.Forms { public class UserControl { } } + namespace Avalonia.Controls { public class UserControl { } } + namespace Microsoft.Maui.Controls { public class ContentPage { } } + + namespace TestNs + { + public sealed class WpfViewModel { } + public sealed class WinFormsViewModel { } + public sealed class AvaloniaViewModel { } + public sealed class MauiViewModel { } + + [IViewFor] + public partial class WpfView : System.Windows.Window { } + + [IViewFor] + public partial class WinFormsView : System.Windows.Forms.UserControl { } + + [IViewFor] + public partial class AvaloniaView : Avalonia.Controls.UserControl { } + + [IViewFor] + public partial class MauiView : Microsoft.Maui.Controls.ContentPage { } + } + """); + + var wpfSource = GetGeneratedSource(generatedSources, "TestNs.WpfView.IViewFor.g.cs"); + var winFormsSource = GetGeneratedSource(generatedSources, "TestNs.WinFormsView.IViewFor.g.cs"); + var avaloniaSource = GetGeneratedSource(generatedSources, "TestNs.AvaloniaView.IViewFor.g.cs"); + var mauiSource = GetGeneratedSource(generatedSources, "TestNs.MauiView.IViewFor.g.cs"); + + await Assert.That(generatedSources.Count).IsEqualTo(SupportedPlatformGeneratedSourceCount); + await Assert.That(wpfSource.Contains("using System.Windows;", StringComparison.Ordinal)).IsTrue(); + await Assert.That(winFormsSource.Contains( + "[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]", + StringComparison.Ordinal)).IsTrue(); + await Assert.That(avaloniaSource.Contains( + "AvaloniaProperty.Register", + StringComparison.Ordinal)).IsTrue(); + await Assert.That(mauiSource.Contains( + "BindableProperty.Create(nameof(ViewModel), typeof(TestNs.MauiViewModel)", + StringComparison.Ordinal)).IsTrue(); + } + + /// Verifies unsupported and non-partial IViewFor targets do not emit view implementations. + /// A task representing the asynchronous assertion work. + [Test] + public async Task DoesNotGenerateViewSourceForUnsupportedOrNonPartialTargets() + { + var generatedSources = RunIViewForGenerator( + """ + using ReactiveUI.SourceGenerators; + + namespace TestNs + { + public sealed class ViewModel { } + + [IViewFor] + public partial class UnsupportedView { } + + [IViewFor] + public class NonPartialView { } + } + """); + + await Assert.That(generatedSources.Count).IsEqualTo(NoViewImplementationGeneratedSourceCount); + await Assert.That(generatedSources.Any(static source => source.HintName.EndsWith(".IViewFor.g.cs", StringComparison.Ordinal))).IsFalse(); + await Assert.That(GetGeneratedSource(generatedSources, "ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.cs").Contains("UnsupportedView", StringComparison.Ordinal)).IsFalse(); + await Assert.That(GetGeneratedSource(generatedSources, "ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.cs").Contains("NonPartialView", StringComparison.Ordinal)).IsFalse(); + } + + /// Runs the IViewFor generator and returns its emitted sources for focused assertions. + /// The consumer source text. + /// The generated source results. + private static ImmutableArray RunIViewForGenerator(string source) + { + var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + var compilation = CSharpCompilation.Create( + "IViewForConsumer", + [CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8), parseOptions)], + TestCompilationReferences.CreatePortableDefault(), + new(OutputKind.DynamicallyLinkedLibrary)); + GeneratorDriver driver = CSharpGeneratorDriver.Create([new IViewForGenerator()]).WithUpdatedParseOptions(parseOptions); + driver = driver.RunGenerators(compilation); + + return driver.GetRunResult().Results.Single().GeneratedSources; + } + + /// Gets one generated source by its stable hint name. + /// The sources emitted by the generator. + /// The source hint name to find. + /// The generated source text. + private static string GetGeneratedSource(ImmutableArray generatedSources, string hintName) + { + foreach (var source in generatedSources) + { + if (source.HintName == hintName) + { + return source.SourceText.ToString(); + } + } + + throw new InvalidOperationException($"The IViewFor generator did not produce '{hintName}'."); + } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ImmutableArrayBuilderTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ImmutableArrayBuilderTests.cs index 6a5a0b18..cc448fd9 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ImmutableArrayBuilderTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ImmutableArrayBuilderTests.cs @@ -1,20 +1,60 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for . -/// +/// Unit tests for . public sealed class ImmutableArrayBuilderTests { - /// - /// A freshly rented builder starts with Count == 0. - /// + /// The expected answer value. + private const int Answer = 42; + + /// The first item index. + private const int FirstIndex = 0; + + /// The second item index. + private const int SecondIndex = 1; + + /// The count for two items. + private const int TwoItems = 2; + + /// The count for three items. + private const int ThreeItems = 3; + + /// The count for four items. + private const int FourItems = 4; + + /// The count for five items. + private const int FiveItems = 5; + + /// The first WrittenSpan test value. + private const int Seven = 7; + + /// The second WrittenSpan test value. + private const int Eight = 8; + + /// The first ordered test value. + private const int Ten = 10; + + /// The second ordered test value. + private const int Twenty = 20; + + /// The third ordered test value. + private const int Thirty = 30; + + /// The final index in a hundred item collection. + private const int NinetyNine = 99; + + /// The collection growth count. + private const int OneHundred = 100; + + /// The second enumerable test value. + private const int TwoHundred = 200; + + /// A freshly rented builder starts with Count == 0. /// A task to monitor the async. [Test] public async Task WhenRentedThenCountIsZero() @@ -28,9 +68,7 @@ public async Task WhenRentedThenCountIsZero() await Assert.That(count).IsEqualTo(0); } - /// - /// Adding a single item increments Count to 1. - /// + /// Adding a single item increments Count to 1. /// A task to monitor the async. [Test] public async Task WhenItemAddedThenCountIncrements() @@ -38,16 +76,14 @@ public async Task WhenItemAddedThenCountIncrements() int count; using (var builder = ImmutableArrayBuilder.Rent()) { - builder.Add(42); + builder.Add(Answer); count = builder.Count; } await Assert.That(count).IsEqualTo(1); } - /// - /// Multiple adds are reflected in Count. - /// + /// Multiple adds are reflected in Count. /// A task to monitor the async. [Test] public async Task WhenMultipleItemsAddedThenCountMatchesAdded() @@ -56,17 +92,15 @@ public async Task WhenMultipleItemsAddedThenCountMatchesAdded() using (var builder = ImmutableArrayBuilder.Rent()) { builder.Add(1); - builder.Add(2); - builder.Add(3); + builder.Add(TwoItems); + builder.Add(ThreeItems); count = builder.Count; } - await Assert.That(count).IsEqualTo(3); + await Assert.That(count).IsEqualTo(ThreeItems); } - /// - /// ToImmutable returns an array containing all added items in order. - /// + /// ToImmutable returns an array containing all added items in order. /// A task to monitor the async. [Test] public async Task WhenToImmutableCalledThenContainsAddedItems() @@ -74,21 +108,19 @@ public async Task WhenToImmutableCalledThenContainsAddedItems() ImmutableArray result; using (var builder = ImmutableArrayBuilder.Rent()) { - builder.Add(10); - builder.Add(20); - builder.Add(30); + builder.Add(Ten); + builder.Add(Twenty); + builder.Add(Thirty); result = builder.ToImmutable(); } - await Assert.That(result.Length).IsEqualTo(3); - await Assert.That(result[0]).IsEqualTo(10); - await Assert.That(result[1]).IsEqualTo(20); - await Assert.That(result[2]).IsEqualTo(30); + await Assert.That(result.Length).IsEqualTo(ThreeItems); + await Assert.That(result[FirstIndex]).IsEqualTo(Ten); + await Assert.That(result[SecondIndex]).IsEqualTo(Twenty); + await Assert.That(result[TwoItems]).IsEqualTo(Thirty); } - /// - /// ToArray returns a mutable array with the same elements. - /// + /// ToArray returns a mutable array with the same elements. /// A task to monitor the async. [Test] public async Task WhenToArrayCalledThenReturnsMutableArray() @@ -101,14 +133,12 @@ public async Task WhenToArrayCalledThenReturnsMutableArray() result = builder.ToArray(); } - await Assert.That(result.Length).IsEqualTo(2); - await Assert.That(result[0]).IsEqualTo("a"); - await Assert.That(result[1]).IsEqualTo("b"); + await Assert.That(result.Length).IsEqualTo(TwoItems); + await Assert.That(result[FirstIndex]).IsEqualTo("a"); + await Assert.That(result[SecondIndex]).IsEqualTo("b"); } - /// - /// AddRange appends all items from the span. - /// + /// AddRange appends all items from the span. /// A task to monitor the async. [Test] public async Task WhenAddRangeCalledThenAllItemsAppended() @@ -117,49 +147,40 @@ public async Task WhenAddRangeCalledThenAllItemsAppended() ImmutableArray result; using (var builder = ImmutableArrayBuilder.Rent()) { - ReadOnlySpan items = [1, 2, 3, 4, 5]; + ReadOnlySpan items = [1, TwoItems, ThreeItems, FourItems, FiveItems]; builder.AddRange(items); count = builder.Count; result = builder.ToImmutable(); } - await Assert.That(count).IsEqualTo(5); - await Assert.That(result[4]).IsEqualTo(5); + await Assert.That(count).IsEqualTo(FiveItems); + await Assert.That(result[FourItems]).IsEqualTo(FiveItems); } - /// - /// WrittenSpan reflects the items added so far. - /// + /// WrittenSpan reflects the items added so far. + /// A task to monitor the async. [Test] - public void WhenWrittenSpanAccessedThenReflectsCurrentItems() + public async Task WhenWrittenSpanAccessedThenReflectsCurrentItems() { int length; int first; int second; using (var builder = ImmutableArrayBuilder.Rent()) { - builder.Add(7); - builder.Add(8); + builder.Add(Seven); + builder.Add(Eight); var span = builder.WrittenSpan; length = span.Length; - first = span[0]; - second = span[1]; + first = span[FirstIndex]; + second = span[SecondIndex]; } - if (length != 2) - { - throw new InvalidOperationException($"Expected WrittenSpan.Length 2, got {length}."); - } - - if (first != 7 || second != 8) - { - throw new InvalidOperationException($"Expected 7, 8 but got {first}, {second}."); - } + await Assert.That(length).IsEqualTo(TwoItems); + await Assert.That(first).IsEqualTo(Seven); + await Assert.That(second).IsEqualTo(Eight); } - /// - /// AsEnumerable returns an IEnumerable containing all added items. - /// + /// AsEnumerable returns an IEnumerable containing all added items. /// A task to monitor the async. [Test] public async Task WhenAsEnumerableCalledThenYieldsAllItems() @@ -167,19 +188,17 @@ public async Task WhenAsEnumerableCalledThenYieldsAllItems() List list; using (var builder = ImmutableArrayBuilder.Rent()) { - builder.Add(100); - builder.Add(200); - list = builder.AsEnumerable().ToList(); + builder.Add(OneHundred); + builder.Add(TwoHundred); + list = [.. builder.AsEnumerable()]; } - await Assert.That(list.Count).IsEqualTo(2); - await Assert.That(list[0]).IsEqualTo(100); - await Assert.That(list[1]).IsEqualTo(200); + await Assert.That(list.Count).IsEqualTo(TwoItems); + await Assert.That(list[FirstIndex]).IsEqualTo(OneHundred); + await Assert.That(list[SecondIndex]).IsEqualTo(TwoHundred); } - /// - /// Builder can hold more than the initial capacity (pool growth). - /// + /// Builder can hold more than the initial capacity (pool growth). /// A task to monitor the async. [Test] public async Task WhenManyItemsAddedThenBuilderGrowsCorrectly() @@ -188,7 +207,7 @@ public async Task WhenManyItemsAddedThenBuilderGrowsCorrectly() ImmutableArray result; using (var builder = ImmutableArrayBuilder.Rent()) { - for (var i = 0; i < 100; i++) + for (var i = 0; i < OneHundred; i++) { builder.Add(i); } @@ -197,13 +216,11 @@ public async Task WhenManyItemsAddedThenBuilderGrowsCorrectly() result = builder.ToImmutable(); } - await Assert.That(count).IsEqualTo(100); - await Assert.That(result[99]).IsEqualTo(99); + await Assert.That(count).IsEqualTo(OneHundred); + await Assert.That(result[NinetyNine]).IsEqualTo(NinetyNine); } - /// - /// ToImmutable on an empty builder returns an empty ImmutableArray. - /// + /// ToImmutable on an empty builder returns an empty ImmutableArray. /// A task to monitor the async. [Test] public async Task WhenEmptyThenToImmutableReturnsEmpty() @@ -217,9 +234,7 @@ public async Task WhenEmptyThenToImmutableReturnsEmpty() await Assert.That(result.IsEmpty).IsTrue(); } - /// - /// ToString returns the WrittenSpan string representation without throwing. - /// + /// ToString returns the WrittenSpan string representation without throwing. /// A task to monitor the async. [Test] public async Task WhenToStringCalledThenDoesNotThrow() @@ -235,21 +250,21 @@ public async Task WhenToStringCalledThenDoesNotThrow() await Assert.That(result).IsNotNull(); } - /// - /// Dispose can be called multiple times without throwing. - /// + /// Dispose can be called multiple times without throwing. + /// A task to monitor the async. [Test] - public void WhenDisposedTwiceThenDoesNotThrow() + public async Task WhenDisposedTwiceThenDoesNotThrow() { var builder = ImmutableArrayBuilder.Rent(); builder.Add(1); + var completed = false; builder.Dispose(); builder.Dispose(); + completed = true; + await Assert.That(completed).IsTrue(); } - /// - /// AddRange followed by Add correctly appends items in order. - /// + /// AddRange followed by Add correctly appends items in order. /// A task to monitor the async. [Test] public async Task WhenAddRangeThenAddThenOrderPreserved() @@ -257,13 +272,13 @@ public async Task WhenAddRangeThenAddThenOrderPreserved() ImmutableArray result; using (var builder = ImmutableArrayBuilder.Rent()) { - ReadOnlySpan range = [1, 2, 3]; + ReadOnlySpan range = [1, TwoItems, ThreeItems]; builder.AddRange(range); - builder.Add(4); + builder.Add(FourItems); result = builder.ToImmutable(); } - await Assert.That(result.Length).IsEqualTo(4); - await Assert.That(result[3]).IsEqualTo(4); + await Assert.That(result.Length).IsEqualTo(FourItems); + await Assert.That(result[ThreeItems]).IsEqualTo(FourItems); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs index 8797462e..81bb71ae 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for the ObservableAsProperty generator. -/// +/// Unit tests for the ObservableAsProperty generator. public class OAPFromObservableGeneratorTests : TestBase { - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task FromObservableProp() @@ -37,9 +32,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task Nested() @@ -82,9 +75,7 @@ public partial class TestVMInner3 : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task FromObservableMethods() @@ -109,9 +100,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task FromObservableMethodsWithName() @@ -136,9 +125,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task WithName() @@ -163,9 +150,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task WithAttr() @@ -194,9 +179,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task AttrRef() @@ -225,9 +208,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task AttrNullRef() @@ -256,9 +237,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// /// A task to monitor the async. /// @@ -297,9 +276,7 @@ public TestVM() return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// /// A task to monitor the async. /// diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs index d5addf82..bee6c2c0 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for the ObservableAsProperty generator. -/// +/// Unit tests for the ObservableAsProperty generator. public class OAPGeneratorTests : TestBase { - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task FromField() @@ -37,9 +32,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task FromFieldNestedClass() @@ -82,9 +75,7 @@ public partial class TestVMInner3 : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task NonReadOnlyFromField() @@ -109,9 +100,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task NamedFromField() @@ -136,9 +125,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates observable properties. - /// + /// Tests that the source generator correctly generates observable properties. /// A task to monitor the async. [Test] public Task NonReadOnlyFromFieldProtected() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs index b143937b..94176492 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for the ObservableAsProperty generator covering edge cases. -/// +/// Extended unit tests for the ObservableAsProperty generator covering edge cases. public class OapExtTests : TestBase { - /// - /// Tests ObservableAsProperty with initial value. - /// + /// Tests ObservableAsProperty with initial value. /// A task to monitor the async. [Test] public Task FromFieldWithInitialValue() @@ -35,9 +30,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with initial value for numeric type. - /// + /// Tests ObservableAsProperty with initial value for numeric type. /// A task to monitor the async. [Test] public Task FromFieldWithNumericInitialValue() @@ -62,9 +55,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with combined options. - /// + /// Tests ObservableAsProperty with combined options. /// A task to monitor the async. [Test] public Task FromFieldWithCombinedOptions() @@ -86,9 +77,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with generic types. - /// + /// Tests ObservableAsProperty with generic types. /// A task to monitor the async. [Test] public Task FromFieldWithGenericTypes() @@ -117,9 +106,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty from observable property with generic type. - /// + /// Tests ObservableAsProperty from observable property with generic type. /// A task to monitor the async. [Test] public Task FromObservablePropertyWithGenericType() @@ -147,9 +134,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with nullable types. - /// + /// Tests ObservableAsProperty with nullable types. /// A task to monitor the async. [Test] public Task FromFieldWithNullableTypes() @@ -177,9 +162,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty from observable method with parameters converted. - /// + /// Tests ObservableAsProperty from observable method with parameters converted. /// A task to monitor the async. [Test] public Task FromObservableMethodWithCustomName() @@ -205,9 +188,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with tuple types. - /// + /// Tests ObservableAsProperty with tuple types. /// A task to monitor the async. [Test] public Task FromFieldWithTupleTypes() @@ -232,9 +213,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty from observable property with tuple. - /// + /// Tests ObservableAsProperty from observable property with tuple. /// A task to monitor the async. [Test] public Task FromObservablePropertyWithTuple() @@ -258,9 +237,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with array types. - /// + /// Tests ObservableAsProperty with array types. /// A task to monitor the async. [Test] public Task FromFieldWithArrayTypes() @@ -288,9 +265,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty in generic class. - /// + /// Tests ObservableAsProperty in generic class. /// A task to monitor the async. [Test] public Task FromFieldInGenericClass() @@ -316,9 +291,7 @@ public partial class GenericVM : ReactiveObject where T : class return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with enum types. - /// + /// Tests ObservableAsProperty with enum types. /// A task to monitor the async. [Test] public Task FromFieldWithEnumTypes() @@ -345,9 +318,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty from observable property with enum. - /// + /// Tests ObservableAsProperty from observable property with enum. /// A task to monitor the async. [Test] public Task FromObservablePropertyWithEnum() @@ -373,9 +344,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests multiple ObservableAsProperty in same class. - /// + /// Tests multiple ObservableAsProperty in same class. /// A task to monitor the async. [Test] public Task FromMultipleObservableAsProperties() @@ -416,9 +385,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty in record class. - /// + /// Tests ObservableAsProperty in record class. /// A task to monitor the async. [Test] public Task FromFieldInRecordClass() @@ -444,9 +411,7 @@ public partial record TestVMRecord : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with interface types. - /// + /// Tests ObservableAsProperty with interface types. /// A task to monitor the async. [Test] public Task FromFieldWithInterfaceTypes() @@ -480,9 +445,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty from observable property with interface. - /// + /// Tests ObservableAsProperty from observable property with interface. /// A task to monitor the async. [Test] public Task FromObservablePropertyWithInterface() @@ -510,9 +473,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with TimeSpan and DateTimeOffset. - /// + /// Tests ObservableAsProperty with TimeSpan and DateTimeOffset. /// A task to monitor the async. [Test] public Task FromFieldWithTimeTypes() @@ -540,9 +501,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty in deeply nested class. - /// + /// Tests ObservableAsProperty in deeply nested class. /// A task to monitor the async. [Test] public Task FromFieldInDeeplyNestedClass() @@ -589,9 +548,7 @@ public partial class Level4 : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with delegate types. - /// + /// Tests ObservableAsProperty with delegate types. /// A task to monitor the async. [Test] public Task FromFieldWithDelegateTypes() @@ -619,9 +576,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with attributes. - /// + /// Tests ObservableAsProperty with attributes. /// A task to monitor the async. [Test] public Task FromFieldWithAttributes() @@ -654,9 +609,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ObservableAsProperty with complex nested generics from observable. - /// + /// Tests ObservableAsProperty with complex nested generics from observable. /// A task to monitor the async. [Test] public Task FromObservablePropertyWithComplexGenerics() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs index 95ac367c..cbcd36d7 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs @@ -1,20 +1,22 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for . -/// +/// Extended unit tests for . public sealed class PropAnalyzerExtTests { - /// - /// Validates a static property does not trigger the diagnostic. - /// + /// Identifies the diagnostic validated by this test class. + private const string ReactiveFieldDiagnosticId = "RXUISG0016"; + + /// Defines the expected number of diagnostics for the multi-diagnostic test. + private const int ExpectedDiagnosticCount = 3; + + /// Validates a static property does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void StaticNoDiag() + public async Task StaticNoDiag() { const string source = """ using ReactiveUI; @@ -27,16 +29,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a property with private setter does not trigger the diagnostic. - /// + /// Validates a property with private setter does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenPrivateSetterThenDoesNotReportDiagnostic() + public async Task WhenPrivateSetterThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -49,16 +50,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a property with internal setter does not trigger the diagnostic. - /// + /// Validates a property with internal setter does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenInternalSetterThenDoesNotReportDiagnostic() + public async Task WhenInternalSetterThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -71,16 +71,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a read-only property does not trigger the diagnostic. - /// + /// Validates a read-only property does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReadOnlyPropertyThenDoesNotReportDiagnostic() + public async Task WhenReadOnlyPropertyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -93,16 +92,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a computed property does not trigger the diagnostic. - /// + /// Validates a computed property does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenComputedPropertyThenDoesNotReportDiagnostic() + public async Task WhenComputedPropertyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -116,16 +114,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a property with getter body does not trigger the diagnostic. - /// + /// Validates a property with getter body does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenPropertyWithGetterBodyThenDoesNotReportDiagnostic() + public async Task WhenPropertyWithGetterBodyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -143,16 +140,15 @@ public string Name } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a ReactiveCommand property type is ignored. - /// + /// Validates a ReactiveCommand property type is ignored. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveCommandPropertyThenDoesNotReportDiagnostic() + public async Task WhenReactiveCommandPropertyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -166,16 +162,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a ViewModelActivator property type is ignored. - /// + /// Validates a ViewModelActivator property type is ignored. + /// A task that represents the asynchronous test operation. [Test] - public void WhenViewModelActivatorPropertyThenDoesNotReportDiagnostic() + public async Task WhenViewModelActivatorPropertyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -188,16 +183,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a property already annotated with ObservableAsProperty is ignored. - /// + /// Validates a property already annotated with ObservableAsProperty is ignored. + /// A task that represents the asynchronous test operation. [Test] - public void WhenObservableAsPropertyPresentThenDoesNotReportDiagnostic() + public async Task WhenObservableAsPropertyPresentThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -212,16 +206,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates multiple public auto-properties trigger multiple diagnostics. - /// + /// Validates multiple public auto-properties trigger multiple diagnostics. + /// A task that represents the asynchronous test operation. [Test] - public void WhenMultiplePublicAutoPropertiesThenReportsMultipleDiagnostics() + public async Task WhenMultiplePublicAutoPropertiesThenReportsMultipleDiagnostics() { const string source = """ using ReactiveUI; @@ -236,16 +229,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDiagnosticCount(diagnostics, "RXUISG0016", 3); + await AssertDiagnosticCount(diagnostics, ReactiveFieldDiagnosticId, ExpectedDiagnosticCount); } - /// - /// Validates non-ReactiveObject class does not trigger the diagnostic. - /// + /// Validates non-ReactiveObject class does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenNotReactiveObjectThenDoesNotReportDiagnostic() + public async Task WhenNotReactiveObjectThenDoesNotReportDiagnostic() { const string source = """ namespace TestNs; @@ -256,16 +248,15 @@ public class TestVM } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates IReactiveObject implementation triggers the diagnostic. - /// + /// Validates IReactiveObject implementation triggers the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenIReactiveObjectThenReportsDiagnostic() + public async Task WhenIReactiveObjectThenReportsDiagnostic() { const string source = """ using ReactiveUI; @@ -281,16 +272,15 @@ public void RaisePropertyChanged(PropertyChangedEventArgs args) { } } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0016"); + await AssertContainsDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates nested class inheriting ReactiveObject triggers the diagnostic. - /// + /// Validates nested class inheriting ReactiveObject triggers the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenNestedReactiveObjectThenReportsDiagnostic() + public async Task WhenNestedReactiveObjectThenReportsDiagnostic() { const string source = """ using ReactiveUI; @@ -306,16 +296,15 @@ public partial class InnerVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0016"); + await AssertContainsDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates protected property does not trigger the diagnostic. - /// + /// Validates protected property does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenProtectedPropertyThenDoesNotReportDiagnostic() + public async Task WhenProtectedPropertyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -328,16 +317,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates internal property does not trigger the diagnostic. - /// + /// Validates internal property does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenInternalPropertyThenDoesNotReportDiagnostic() + public async Task WhenInternalPropertyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -350,16 +338,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates private property does not trigger the diagnostic. - /// + /// Validates private property does not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenPrivatePropertyThenDoesNotReportDiagnostic() + public async Task WhenPrivatePropertyThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -372,16 +359,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates property in class directly inheriting ReactiveObject triggers the diagnostic. - /// + /// Validates property in class directly inheriting ReactiveObject triggers the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void DirectInherit() + public async Task DirectInherit() { const string source = """ using ReactiveUI; @@ -394,16 +380,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0016"); + await AssertContainsDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates init-only property triggers the diagnostic (has init setter). - /// + /// Validates init-only property triggers the diagnostic (has init setter). + /// A task that represents the asynchronous test operation. [Test] - public void InitOnlyDiag() + public async Task InitOnlyDiag() { const string source = """ using ReactiveUI; @@ -416,17 +401,16 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); // Init-only properties have a setter (init), so the analyzer reports them - AssertContainsDiagnostic(diagnostics, "RXUISG0016"); + await AssertContainsDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates required property triggers the diagnostic. - /// + /// Validates required property triggers the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void RequiredDiag() + public async Task RequiredDiag() { const string source = """ using ReactiveUI; @@ -439,12 +423,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0016"); + await AssertContainsDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - private static Diagnostic[] GetDiagnostics(string source) + /// Gets diagnostics produced by the analyzer for the supplied source. + /// The source to analyze. + /// A task that resolves to the diagnostics. + private static async Task GetDiagnostics(string source) { var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13)); @@ -452,36 +439,68 @@ private static Diagnostic[] GetDiagnostics(string source) assemblyName: "AnalyzerTests", syntaxTrees: [syntaxTree], references: TestCompilationReferences.CreateDefault(), - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); var analyzer = new PropertyToReactiveFieldAnalyzer(); - var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); - return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray(); + var compilationWithAnalyzers = compilation.WithAnalyzers([analyzer]); + return (await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync()).ToArray(); } - private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics contain the specified ID. + /// The diagnostics to inspect. + /// The expected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (!diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsTrue(); } - private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics do not contain the specified ID. + /// The diagnostics to inspect. + /// The unexpected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsFalse(); } - private static void AssertDiagnosticCount(IEnumerable diagnostics, string diagnosticId, int expectedCount) + /// Asserts that the diagnostics contain the expected number of occurrences of an ID. + /// The diagnostics to inspect. + /// The diagnostic ID to count. + /// The expected number of occurrences. + /// A task that represents the assertion. + private static async Task AssertDiagnosticCount(IEnumerable diagnostics, string diagnosticId, int expectedCount) { - var actualCount = diagnostics.Count(d => d.Id == diagnosticId); - if (actualCount != expectedCount) + var actualCount = 0; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Expected {expectedCount} '{diagnosticId}' diagnostics but found {actualCount}."); + if (diagnostic.Id == diagnosticId) + { + actualCount++; + } } + + await Assert.That(actualCount).IsEqualTo(expectedCount); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs index 2bc6acaa..0b392536 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs @@ -1,18 +1,16 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for . -/// +/// Unit tests for . public sealed class PropertyToReactiveFieldAnalyzerTests { - /// - /// Validates the analyzer rejects a null analysis context. - /// + /// Identifies the diagnostic validated by this test class. + private const string ReactiveFieldDiagnosticId = "RXUISG0016"; + + /// Validates the analyzer rejects a null analysis context. [Test] public void InitializeWithNullContextThrows() { @@ -28,11 +26,10 @@ public void InitializeWithNullContextThrows() } } - /// - /// Validates a public auto-property triggers the suggestion to convert it into a reactive field. - /// + /// Validates a public auto-property triggers the suggestion to convert it into a reactive field. + /// A task that represents the asynchronous test operation. [Test] - public void WhenPublicAutoPropertyThenReportsDiagnostic() + public async Task WhenPublicAutoPropertyThenReportsDiagnostic() { const string source = """ using ReactiveUI; @@ -45,16 +42,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0016"); + await AssertContainsDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates a property already annotated with [Reactive] is ignored. - /// + /// Validates a property already annotated with [Reactive] is ignored. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveAttributePresentThenDoesNotReportDiagnostic() + public async Task WhenReactiveAttributePresentThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -69,16 +65,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates the syntax-based Reactive attribute fallback handles qualified names. - /// + /// Validates the syntax-based Reactive attribute fallback handles qualified names. + /// A task that represents the asynchronous test operation. [Test] - public void WhenQualifiedReactiveAttributePresentThenDoesNotReportDiagnostic() + public async Task WhenQualifiedReactiveAttributePresentThenDoesNotReportDiagnostic() { const string source = """ using ReactiveUI; @@ -92,16 +87,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - /// - /// Validates the analyzer recognizes a fully qualified ReactiveObject base type. - /// + /// Validates the analyzer recognizes a fully qualified ReactiveObject base type. + /// A task that represents the asynchronous test operation. [Test] - public void WhenQualifiedReactiveBaseTypeThenReportsDiagnostic() + public async Task WhenQualifiedReactiveBaseTypeThenReportsDiagnostic() { const string source = """ namespace TestNs; @@ -112,12 +106,15 @@ public partial class TestVM : ReactiveUI.ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0016"); + await AssertContainsDiagnostic(diagnostics, ReactiveFieldDiagnosticId); } - private static Diagnostic[] GetDiagnostics(string source) + /// Gets diagnostics produced by the analyzer for the supplied source. + /// The source to analyze. + /// A task that resolves to the diagnostics. + private static async Task GetDiagnostics(string source) { var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13)); @@ -125,27 +122,49 @@ private static Diagnostic[] GetDiagnostics(string source) assemblyName: "AnalyzerTests", syntaxTrees: [syntaxTree], references: TestCompilationReferences.CreateDefault(), - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); var analyzer = new PropertyToReactiveFieldAnalyzer(); - var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); - return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray(); + var compilationWithAnalyzers = compilation.WithAnalyzers([analyzer]); + return (await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync()).ToArray(); } - private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics contain the specified ID. + /// The diagnostics to inspect. + /// The expected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (!diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsTrue(); } - private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics do not contain the specified ID. + /// The diagnostics to inspect. + /// The unexpected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsFalse(); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs index 90e2612e..3b912c2b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs @@ -1,48 +1,36 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis.CodeFixes; namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for . -/// +/// Unit tests for . public sealed class PropertyToReactiveFieldCodeFixProviderTests { - /// - /// Validates the code fix provider advertises the expected diagnostic ID. - /// + /// Validates the code fix provider advertises the expected diagnostic ID. + /// A task that represents the asynchronous test operation. [Test] - public void FixableDiagnosticIdsIncludesReactiveFieldRule() + public async Task FixableDiagnosticIdsIncludesReactiveFieldRule() { var provider = new PropertyToReactiveFieldCodeFixProvider(); - if (!provider.FixableDiagnosticIds.Contains("RXUISG0016")) - { - throw new InvalidOperationException("Expected RXUISG0016 to be fixable."); - } + await Assert.That(provider.FixableDiagnosticIds.Contains("RXUISG0016")).IsTrue(); } - /// - /// Validates the code fix provider exposes a fix-all implementation. - /// + /// Validates the code fix provider exposes a fix-all implementation. + /// A task that represents the asynchronous test operation. [Test] - public void GetFixAllProviderReturnsBatchFixer() + public async Task GetFixAllProviderReturnsBatchFixer() { var provider = new PropertyToReactiveFieldCodeFixProvider(); - if (provider.GetFixAllProvider() is null) - { - throw new InvalidOperationException("Expected a fix-all provider."); - } + await Assert.That(provider.GetFixAllProvider()).IsNotNull(); } - /// - /// Validates a public auto-property is converted to a private field annotated with [Reactive]. - /// + /// Validates a public auto-property is converted to a private field annotated with [Reactive]. + /// A task that represents the asynchronous test operation. [Test] - public void WhenApplyingFixThenConvertsPropertyToReactiveField() + public async Task WhenApplyingFixThenConvertsPropertyToReactiveField() { const string source = """ using ReactiveUI; @@ -55,14 +43,17 @@ public partial class TestVM : ReactiveObject } """; - var fixedSource = ApplyFix(source); + var fixedSource = await ApplyFix(source); - AssertContains(fixedSource, "[ReactiveUI.SourceGenerators.Reactive]"); - AssertContains(fixedSource, "private bool _isVisible"); - AssertDoesNotContain(fixedSource, "public bool IsVisible"); + await Assert.That(fixedSource.Contains("[ReactiveUI.SourceGenerators.Reactive]", StringComparison.Ordinal)).IsTrue(); + await Assert.That(fixedSource.Contains("private bool _isVisible", StringComparison.Ordinal)).IsTrue(); + await Assert.That(fixedSource.Contains("public bool IsVisible", StringComparison.Ordinal)).IsFalse(); } - private static string ApplyFix(string source) + /// Applies the code fix to the supplied source. + /// The source to fix. + /// A task that resolves to the fixed source. + private static async Task ApplyFix(string source) { var tree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13)); @@ -71,12 +62,10 @@ private static string ApplyFix(string source) "CodeFixTests", syntaxTrees: [tree], references: TestCompilationReferences.CreateDefault(), - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); - var diagnostic = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)) - .GetAnalyzerDiagnosticsAsync() - .GetAwaiter().GetResult() - .Single(d => d.Id == "RXUISG0016"); + var diagnostics = await compilation.WithAnalyzers([analyzer]).GetAnalyzerDiagnosticsAsync(); + var diagnostic = diagnostics.Single(static d => d.Id == "RXUISG0016"); using var workspace = new AdhocWorkspace(); var project = workspace.CurrentSolution @@ -100,28 +89,12 @@ private static string ApplyFix(string source) (a, _) => actions.Add(a), CancellationToken.None); - provider.RegisterCodeFixesAsync(context).GetAwaiter().GetResult(); + await provider.RegisterCodeFixesAsync(context); - var operation = actions.Single().GetOperationsAsync(CancellationToken.None).GetAwaiter().GetResult().Single(); + var operation = (await actions[0].GetOperationsAsync(CancellationToken.None))[0]; operation.Apply(document.Project.Solution.Workspace, CancellationToken.None); var updatedDoc = document.Project.Solution.Workspace.CurrentSolution.GetDocument(document.Id); - return updatedDoc!.GetTextAsync(CancellationToken.None).GetAwaiter().GetResult().ToString(); - } - - private static void AssertContains(string actual, string expected) - { - if (!actual.Contains(expected, StringComparison.Ordinal)) - { - throw new InvalidOperationException($"Expected output to contain '{expected}'."); - } - } - - private static void AssertDoesNotContain(string actual, string unexpected) - { - if (actual.Contains(unexpected, StringComparison.Ordinal)) - { - throw new InvalidOperationException($"Expected output not to contain '{unexpected}'."); - } + return (await updatedDoc!.GetTextAsync(CancellationToken.None)).ToString(); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs index fafaa784..90d5d389 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs @@ -1,18 +1,16 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for . -/// +/// Unit tests for . public sealed class ReactiveAttributeMisuseAnalyzerTests { - /// - /// Validates the analyzer rejects a null analysis context. - /// + /// Identifies the diagnostic validated by this test class. + private const string ReactivePartialDiagnosticId = "RXUISG0020"; + + /// Validates the analyzer rejects a null analysis context. [Test] public void InitializeWithNullContextThrows() { @@ -28,11 +26,10 @@ public void InitializeWithNullContextThrows() } } - /// - /// Verifies a non-partial property annotated with [Reactive] produces a warning. - /// + /// Verifies a non-partial property annotated with [Reactive] produces a warning. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnNonPartialPropertyThenWarn() + public async Task WhenReactiveOnNonPartialPropertyThenWarn() { const string source = """ using ReactiveUI; @@ -47,16 +44,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies a non-partial containing type annotated with a [Reactive] property produces a warning. - /// + /// Verifies a non-partial containing type annotated with a [Reactive] property produces a warning. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnNonPartialContainingTypeThenWarn() + public async Task WhenReactiveOnNonPartialContainingTypeThenWarn() { const string source = """ using ReactiveUI; @@ -71,16 +67,15 @@ public class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies no warning is produced when both property and containing type are partial. - /// + /// Verifies no warning is produced when both property and containing type are partial. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveOnPartialPropertyAndTypeThenNoWarn() + public async Task WhenReactiveOnPartialPropertyAndTypeThenNoWarn() { const string source = """ using ReactiveUI; @@ -95,16 +90,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies the analyzer recognizes the explicit ReactiveAttribute name. - /// + /// Verifies the analyzer recognizes the explicit ReactiveAttribute name. + /// A task that represents the asynchronous test operation. [Test] - public void WhenReactiveAttributeSuffixUsedThenWarns() + public async Task WhenReactiveAttributeSuffixUsedThenWarns() { const string source = """ using ReactiveUI; @@ -119,16 +113,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertContainsDiagnostic(diagnostics, "RXUISG0020"); + await AssertContainsDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - /// - /// Verifies unrelated attributes do not trigger the diagnostic. - /// + /// Verifies unrelated attributes do not trigger the diagnostic. + /// A task that represents the asynchronous test operation. [Test] - public void WhenOnlyNonReactiveAttributesExistThenDoesNotWarn() + public async Task WhenOnlyNonReactiveAttributesExistThenDoesNotWarn() { const string source = """ using System; @@ -143,12 +136,15 @@ public partial class TestVM : ReactiveObject } """; - var diagnostics = GetDiagnostics(source); + var diagnostics = await GetDiagnostics(source); - AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020"); + await AssertDoesNotContainDiagnostic(diagnostics, ReactivePartialDiagnosticId); } - private static Diagnostic[] GetDiagnostics(string source) + /// Gets diagnostics produced by the analyzer for the supplied source. + /// The source to analyze. + /// A task that resolves to the diagnostics. + private static async Task GetDiagnostics(string source) { var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13)); @@ -159,27 +155,49 @@ private static Diagnostic[] GetDiagnostics(string source) MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), ], - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); var analyzer = new ReactiveAttributeMisuseAnalyzer(); - var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); - return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray(); + var compilationWithAnalyzers = compilation.WithAnalyzers([analyzer]); + return (await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync()).ToArray(); } - private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics contain the specified ID. + /// The diagnostics to inspect. + /// The expected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (!diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsTrue(); } - private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) + /// Asserts that the diagnostics do not contain the specified ID. + /// The diagnostics to inspect. + /// The unexpected diagnostic ID. + /// A task that represents the assertion. + private static async Task AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId) { - if (diagnostics.Any(d => d.Id == diagnosticId)) + var found = false; + foreach (var diagnostic in diagnostics) { - throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly."); + if (diagnostic.Id == diagnosticId) + { + found = true; + break; + } } + + await Assert.That(found).IsFalse(); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs index c4837499..35e3ae5f 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs @@ -1,48 +1,39 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis.CodeFixes; namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for . -/// +/// Unit tests for . public sealed class ReactiveAttributeMisuseCodeFixProviderTests { - /// - /// Validates the code fix provider advertises the expected diagnostic ID. - /// + /// Identifies the diagnostic validated by this test class. + private const string ReactivePartialDiagnosticId = "RXUISG0020"; + + /// Validates the code fix provider advertises the expected diagnostic ID. + /// A task that represents the asynchronous test operation. [Test] - public void FixableDiagnosticIdsIncludesReactivePartialRule() + public async Task FixableDiagnosticIdsIncludesReactivePartialRule() { var provider = new ReactiveAttributeMisuseCodeFixProvider(); - if (!provider.FixableDiagnosticIds.Contains("RXUISG0020")) - { - throw new InvalidOperationException("Expected RXUISG0020 to be fixable."); - } + await Assert.That(provider.FixableDiagnosticIds.Contains(ReactivePartialDiagnosticId)).IsTrue(); } - /// - /// Validates the code fix provider exposes a fix-all implementation. - /// + /// Validates the code fix provider exposes a fix-all implementation. + /// A task that represents the asynchronous test operation. [Test] - public void GetFixAllProviderReturnsBatchFixer() + public async Task GetFixAllProviderReturnsBatchFixer() { var provider = new ReactiveAttributeMisuseCodeFixProvider(); - if (provider.GetFixAllProvider() is null) - { - throw new InvalidOperationException("Expected a fix-all provider."); - } + await Assert.That(provider.GetFixAllProvider()).IsNotNull(); } - /// - /// Verifies `required` stays before `partial` when applying the code fix. - /// + /// Verifies `required` stays before `partial` when applying the code fix. + /// A task that represents the asynchronous test operation. [Test] - public void WhenRequiredPropertyThenPartialInsertedAfterRequired() + public async Task WhenRequiredPropertyThenPartialInsertedAfterRequired() { const string source = """ using ReactiveUI; @@ -57,17 +48,16 @@ public partial class TestVM : ReactiveObject } """; - var fixedSource = ApplyFix(source); + var fixedSource = await ApplyFix(source); - AssertContains(fixedSource, "public required partial string? PartialRequiredPropertyTest"); - AssertDoesNotContain(fixedSource, "public partial required string? PartialRequiredPropertyTest"); + await Assert.That(fixedSource.Contains("public required partial string? PartialRequiredPropertyTest", StringComparison.Ordinal)).IsTrue(); + await Assert.That(fixedSource.Contains("public partial required string? PartialRequiredPropertyTest", StringComparison.Ordinal)).IsFalse(); } - /// - /// Verifies no code fix is registered when the diagnostic location is outside a property declaration. - /// + /// Verifies no code fix is registered when the diagnostic location is outside a property declaration. + /// A task that represents the asynchronous test operation. [Test] - public void WhenDiagnosticDoesNotTargetAPropertyThenNoCodeFixIsRegistered() + public async Task WhenDiagnosticDoesNotTargetAPropertyThenNoCodeFixIsRegistered() { const string source = """ using ReactiveUI; @@ -88,27 +78,45 @@ public class TestVM : ReactiveObject .AddMetadataReference(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)); var document = project.AddDocument("t.cs", source); - var root = document.GetSyntaxRootAsync(CancellationToken.None).GetAwaiter().GetResult()!; - var classDeclaration = root.DescendantNodes().OfType().Single(); - var diagnosticDescriptor = new ReactiveAttributeMisuseAnalyzer().SupportedDiagnostics.Single(d => d.Id == "RXUISG0020"); - var diagnostic = Diagnostic.Create(diagnosticDescriptor, classDeclaration.Identifier.GetLocation()); - var actions = new List(); - var context = new CodeFixContext(document, diagnostic, (a, _) => actions.Add(a), CancellationToken.None); + var root = (await document.GetSyntaxRootAsync(CancellationToken.None))!; + Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax? classDeclaration = null; + foreach (var node in root.DescendantNodes()) + { + if (node is Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax declaration) + { + classDeclaration = declaration; + break; + } + } - var provider = new ReactiveAttributeMisuseCodeFixProvider(); - provider.RegisterCodeFixesAsync(context).GetAwaiter().GetResult(); + await Assert.That(classDeclaration).IsNotNull(); - if (actions.Count != 0) + DiagnosticDescriptor? diagnosticDescriptor = null; + foreach (var descriptor in new ReactiveAttributeMisuseAnalyzer().SupportedDiagnostics) { - throw new InvalidOperationException("Expected no code fixes to be registered."); + if (descriptor.Id == ReactivePartialDiagnosticId) + { + diagnosticDescriptor = descriptor; + break; + } } + + await Assert.That(diagnosticDescriptor).IsNotNull(); + var diagnostic = Diagnostic.Create(diagnosticDescriptor!, classDeclaration!.Identifier.GetLocation()); + var actions = new List(); + var context = new CodeFixContext(document, diagnostic, (a, _) => actions.Add(a), CancellationToken.None); + + var provider = new ReactiveAttributeMisuseCodeFixProvider(); + await provider.RegisterCodeFixesAsync(context); + await Assert.That(actions.Count).IsEqualTo(0); } - private static string ApplyFix(string source) + /// Applies the code fix to the supplied source. + /// The source to fix. + /// A task that resolves to the fixed source. + private static async Task ApplyFix(string source) { var tree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13)); - var root = tree.GetRoot(); - var analyzer = new ReactiveAttributeMisuseAnalyzer(); var compilation = CSharpCompilation.Create( "CodeFixTests", @@ -117,12 +125,10 @@ private static string ApplyFix(string source) MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), ], - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); - var diagnostic = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)) - .GetAnalyzerDiagnosticsAsync() - .GetAwaiter().GetResult() - .Single(d => d.Id == "RXUISG0020"); + var diagnostics = await compilation.WithAnalyzers([analyzer]).GetAnalyzerDiagnosticsAsync(); + var diagnostic = diagnostics.Single(static d => d.Id == ReactivePartialDiagnosticId); using var workspace = new AdhocWorkspace(); var project = workspace.CurrentSolution @@ -143,28 +149,12 @@ private static string ApplyFix(string source) (a, _) => actions.Add(a), CancellationToken.None); - provider.RegisterCodeFixesAsync(context).GetAwaiter().GetResult(); + await provider.RegisterCodeFixesAsync(context); - var operation = actions.Single().GetOperationsAsync(CancellationToken.None).GetAwaiter().GetResult().Single(); + var operation = (await actions[0].GetOperationsAsync(CancellationToken.None))[0]; operation.Apply(document.Project.Solution.Workspace, CancellationToken.None); var updatedDoc = document.Project.Solution.Workspace.CurrentSolution.GetDocument(document.Id); - return updatedDoc!.GetTextAsync(CancellationToken.None).GetAwaiter().GetResult().ToString(); - } - - private static void AssertContains(string actual, string expected) - { - if (!actual.Contains(expected, StringComparison.Ordinal)) - { - throw new InvalidOperationException($"Expected output to contain '{expected}'."); - } - } - - private static void AssertDoesNotContain(string actual, string unexpected) - { - if (actual.Contains(unexpected, StringComparison.Ordinal)) - { - throw new InvalidOperationException($"Expected output not to contain '{unexpected}'."); - } + return (await updatedDoc!.GetTextAsync(CancellationToken.None)).ToString(); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs index 6791a8b5..ff675c6f 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for the ReactiveCommand generator. -/// +/// Unit tests for the ReactiveCommand generator. public class ReactiveCMDGeneratorTests : TestBase { - /// - /// Tests that the source generator correctly generates ReactiveCommands. - /// + /// Tests that the source generator correctly generates ReactiveCommands. /// A task to monitor the async. [Test] public Task Basic() @@ -38,9 +33,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates ReactiveCommands. - /// + /// Tests that the source generator correctly generates ReactiveCommands. /// A task to monitor the async. [Test] public Task WithParam() @@ -67,9 +60,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates ReactiveCommands. - /// + /// Tests that the source generator correctly generates ReactiveCommands. /// A task to monitor the async. [Test] public Task FromReactiveAsyncCommand() @@ -98,9 +89,7 @@ private async Task Test1() return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates ReactiveCommands. - /// + /// Tests that the source generator correctly generates ReactiveCommands. /// A task to monitor the async. [Test] public Task AsyncWithParam() @@ -130,9 +119,7 @@ private async Task Test3(string baseString) return TestHelper.TestPass(sourceCode); } - /// - /// Froms the reactive command with output scheduler. - /// + /// Froms the reactive command with output scheduler. /// A task to monitor the async. [Test] public Task Scheduler() @@ -156,9 +143,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Froms the reactive command with nested classes. - /// + /// Froms the reactive command with nested classes. /// A task to monitor the async. [Test] public Task Nested() @@ -215,9 +200,7 @@ public partial class TestInnerClass3 : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Froms the reactive command with access modifier. - /// + /// Froms the reactive command with access modifier. /// A task to monitor the async. [Test] public Task Access() @@ -236,9 +219,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Froms the type of the reactive command with nullable type and nullable return. - /// + /// Froms the type of the reactive command with nullable type and nullable return. /// A task to monitor the async. [Test] public Task NullableTypeReturn() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs index 90018a5a..b9d0390a 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs @@ -1,19 +1,15 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerator.Tests; namespace ReactiveUI.SourceGenerators.Tests; -/// -/// ReactiveCollectionGeneratorTests. -/// + +/// Tests reactive collection source generation. public class ReactiveCollectionGeneratorTests : TestBase { - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task BasicField() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs index 17bf5751..cb1deae4 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs @@ -1,18 +1,75 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for the Reactive generator. -/// +/// Unit tests for the Reactive generator. public class ReactiveGeneratorTests : TestBase { - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// Source containing nested reactive classes used by the nested-class generation test. + private const string NestedClassSource = """ + using System; + using System.Runtime.Serialization; + using System.Text.Json.Serialization; + using ReactiveUI; + using ReactiveUI.SourceGenerators; + using System.Reactive.Linq; + + namespace TestNs1 + { + /// + /// TestViewModel3. + /// + public partial class TestViewModel3 : ReactiveObject + { + [Reactive] + private float _testVM3Property; + + [Reactive] + private float _testVM3Property2; + + /// + /// TestInnerClass. + /// + public partial class TestInnerClass1 : ReactiveObject + { + [Reactive] + private int _testInner1; + + [Reactive] + private int _testInner11; + } + + /// + /// TestInnerClass. + /// + public partial class TestInnerClass2 : ReactiveObject + { + [Reactive] + private int _testInner2; + + [Reactive] + private int _testInner22; + + /// + /// TestInnerClass4. + /// + /// + public partial class TestInnerClass3 : ReactiveObject + { + [Reactive] + private int _testInner3; + + [Reactive] + private int _testInner33; + } + } + } + } + """; + + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task Basic() @@ -37,9 +94,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task CalledValue() @@ -64,9 +119,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task WithAccess() @@ -91,9 +144,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Froms the reactive properies with attributes. - /// + /// Froms the reactive properies with attributes. /// A task to monitor the async. [Test] public Task WithAttributes() @@ -122,9 +173,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Froms the reactive properties with attributes and access and inheritance. - /// + /// Froms the reactive properties with attributes and access and inheritance. /// A task to monitor the async. [Test] public Task WithAttrAccessInherit() @@ -153,9 +202,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Froms the reactive properties with attributes and access and inheritance. - /// + /// Froms the reactive properties with attributes and access and inheritance. /// A task to monitor the async. [Test] public Task WithIdenticalClass() @@ -196,82 +243,12 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Froms the reactive properties with nested class. - /// + /// Froms the reactive properties with nested class. /// A task to monitor the async. [Test] - public Task WithNestedClass() - { - // Arrange: Setup the source code that matches the generator input expectations. - const string sourceCode = """ - using System; - using System.Runtime.Serialization; - using System.Text.Json.Serialization; - using ReactiveUI; - using ReactiveUI.SourceGenerators; - using System.Reactive.Linq; - - namespace TestNs1 - { - /// - /// TestViewModel3. - /// - public partial class TestViewModel3 : ReactiveObject - { - [Reactive] - private float _testVM3Property; - - [Reactive] - private float _testVM3Property2; - - /// - /// TestInnerClass. - /// - public partial class TestInnerClass1 : ReactiveObject - { - [Reactive] - private int _testInner1; - - [Reactive] - private int _testInner11; - } - - /// - /// TestInnerClass. - /// - public partial class TestInnerClass2 : ReactiveObject - { - [Reactive] - private int _testInner2; - - [Reactive] - private int _testInner22; - - /// - /// TestInnerClass4. - /// - /// - public partial class TestInnerClass3 : ReactiveObject - { - [Reactive] - private int _testInner3; - - [Reactive] - private int _testInner33; - } - } - } - } - """; - - // Act: Initialize the helper and run the generator. Assert: Verify the generated code. - return TestHelper.TestPass(sourceCode); - } + public Task WithNestedClass() => TestHelper.TestPass(NestedClassSource); - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task WithInit() @@ -296,9 +273,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests that the source generator correctly generates reactive properties. - /// + /// Tests that the source generator correctly generates reactive properties. /// A task to monitor the async. [Test] public Task Partial() @@ -323,9 +298,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property generation from a partial class with AlsoNotify support. - /// + /// Tests reactive property generation from a partial class with AlsoNotify support. /// A task to monitor the async. [Test] public Task PartialAlsoNotify() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs index f70f9013..aa38ed15 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for the Reactive generator. -/// +/// Unit tests for the Reactive generator. public class ReactiveObjectGeneratorTests : TestBase { - /// - /// Tests the ReactiveObject generator with IReactiveObjectAttribute. - /// + /// Tests the ReactiveObject generator with IReactiveObjectAttribute. /// A task to monitor the async. [Test] public Task Basic() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveUiIntegrationTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveUiIntegrationTests.cs new file mode 100644 index 00000000..aea9c1b8 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveUiIntegrationTests.cs @@ -0,0 +1,400 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Text; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Models; +using ReactiveUI.SourceGenerators.WinForms; + +namespace ReactiveUI.SourceGenerator.Tests; + +/// Tests ReactiveUI package-profile detection and profile-specific command output. +public sealed class ReactiveUiIntegrationTests +{ + /// The number of source callbacks in a generated CombineLatest subscription. + private const int CombineLatestCallbackCount = 2; + + /// The fully qualified System.Reactive unit type name. + private const string SystemReactiveUnitTypeName = "global::System.Reactive.Unit"; + + /// + /// Verifies that ReactiveUI 24 base output uses Primitives even when System.Reactive is + /// independently available to the consuming compilation. + /// + /// A task representing the asynchronous assertion work. + [Test] + public async Task V24BaseUsesPrimitivesWithoutGeneratedSystemReactiveReferences() + { + var references = TestCompilationReferences.CreateDefault(); + var (compilation, generatedSource) = RunCommandGenerator( + """ + using ReactiveUI; + using ReactiveUI.SourceGenerators; + + namespace Compatibility; + + public partial class ViewModel : ReactiveObject + { + [ReactiveCommand] + private void Save() + { + } + } + """, + references); + + var integration = compilation.GetReactiveUiIntegration(); + + await Assert.That(integration.Api).IsEqualTo(ReactiveUiApi.Primitives); + await Assert.That(integration.Namespace).IsEqualTo("global::ReactiveUI"); + await Assert.That(integration.DeclarationNamespace).IsEqualTo("ReactiveUI"); + await Assert.That(integration.VoidTypeName).IsEqualTo("global::ReactiveUI.Primitives.RxVoid"); + await Assert.That(integration.UsingDirectives).IsEqualTo("using ReactiveUI;"); + await Assert.That(generatedSource.Contains("global::ReactiveUI.Primitives.RxVoid", StringComparison.Ordinal)).IsTrue(); + await Assert.That(generatedSource.Contains("global::System.Reactive", StringComparison.Ordinal)).IsFalse(); + await Assert.That(GetErrors(compilation)).IsEmpty(); + } + + /// Verifies that the ReactiveUI 24 System.Reactive variant selects its moved namespace. + /// A task representing the asynchronous assertion work. + [Test] + public async Task V24ReactiveUsesReactiveNamespaceAndSystemReactiveUnit() + { + var references = TestCompilationReferences.CreateForAssemblies( + typeof(object).Assembly, + typeof(Enumerable).Assembly, + typeof(System.ComponentModel.INotifyPropertyChanged).Assembly, + typeof(ReactiveUI.Reactive.ReactiveObject).Assembly, + typeof(System.Reactive.Unit).Assembly, + typeof(ReactiveCommandGenerator).Assembly); + var (compilation, generatedSource) = RunCommandGenerator( + """ + using ReactiveUI.Reactive; + using ReactiveUI.SourceGenerators; + + namespace Compatibility; + + public partial class ViewModel : ReactiveObject + { + [ReactiveCommand] + private void Save() + { + } + } + """, + references); + + var integration = compilation.GetReactiveUiIntegration(); + + await Assert.That(integration.Api).IsEqualTo(ReactiveUiApi.SystemReactive); + await Assert.That(integration.Namespace).IsEqualTo("global::ReactiveUI.Reactive"); + await Assert.That(integration.DeclarationNamespace).IsEqualTo("ReactiveUI.Reactive"); + await Assert.That(integration.VoidTypeName).IsEqualTo(SystemReactiveUnitTypeName); + await Assert.That(integration.UsingDirectives).IsEqualTo("using ReactiveUI;\nusing ReactiveUI.Reactive;"); + await Assert.That(generatedSource.Contains("global::ReactiveUI.Reactive.ReactiveCommand", StringComparison.Ordinal)).IsTrue(); + await Assert.That(generatedSource.Contains(SystemReactiveUnitTypeName, StringComparison.Ordinal)).IsTrue(); + await Assert.That(GetErrors(compilation)).IsEmpty(); + } + + /// Verifies that the pre-v24 API remains on the original namespace and Unit type. + /// A task representing the asynchronous assertion work. + [Test] + public async Task V23UsesLegacyNamespaceAndSystemReactiveUnit() + { + var legacyReference = CreateLegacyReactiveUiReference(); + var references = TestCompilationReferences.CreateForAssemblies( + typeof(object).Assembly, + typeof(Enumerable).Assembly, + typeof(System.ComponentModel.INotifyPropertyChanged).Assembly, + typeof(System.Reactive.Unit).Assembly, + typeof(ReactiveCommandGenerator).Assembly) + .Add(legacyReference); + var (compilation, generatedSource) = RunCommandGenerator( + """ + using ReactiveUI; + using ReactiveUI.SourceGenerators; + + namespace Compatibility; + + public partial class ViewModel : ReactiveObject + { + [ReactiveCommand] + private void Save() + { + } + } + """, + references); + + var integration = compilation.GetReactiveUiIntegration(); + + await Assert.That(integration.Api).IsEqualTo(ReactiveUiApi.Legacy); + await Assert.That(integration.IsNewerThan22).IsTrue(); + await Assert.That(generatedSource.Contains("global::ReactiveUI.ReactiveCommand", StringComparison.Ordinal)).IsTrue(); + await Assert.That(generatedSource.Contains(SystemReactiveUnitTypeName, StringComparison.Ordinal)).IsTrue(); + await Assert.That(GetErrors(compilation)).IsEmpty(); + } + + /// Verifies routed hosts preserve default content while disposing replaced routed views. + /// A task representing the asynchronous assertion work. + [Test] + public async Task RoutedControlHostEmitsDisposableCompositionBeforeConnecting() + { + const string source = """ + using ReactiveUI.SourceGenerators.WinForms; + using System.ComponentModel; + + namespace Compatibility + { + [RoutedControlHost("System.Windows.Forms.UserControl")] + public partial class RoutedHost + { + private IContainer? components; + private void InitializeComponent() + { + } + } + } + """; + + var (compilation, generatedSource) = + RunWinFormsHostGenerator(source, ".RoutedControlHost.g.cs"); + var disposableRegistrationIndex = generatedSource.IndexOf("_disposables.Add(routeSubscription);", StringComparison.Ordinal); + var connectIndex = generatedSource.IndexOf("routeSubscription.Connect(", StringComparison.Ordinal); + + await Assert.That(GetErrors(compilation)).IsEmpty(); + await Assert.That(disposableRegistrationIndex).IsGreaterThanOrEqualTo(0); + await Assert.That(connectIndex).IsGreaterThan(disposableRegistrationIndex); + await Assert.That(generatedSource.Contains("_routedView?.Dispose();", StringComparison.Ordinal)).IsTrue(); + await Assert.That(generatedSource.Contains("_defaultContent?.Dispose();", StringComparison.Ordinal)).IsFalse(); + await Assert.That(AreCombineLatestCallbacksSerialized(generatedSource)).IsTrue(); + } + + /// Verifies view-model hosts register their combined subscription before it connects. + /// A task representing the asynchronous assertion work. + [Test] + public async Task ViewModelControlHostEmitsDisposableCompositionBeforeConnecting() + { + const string source = """ + using ReactiveUI.SourceGenerators.WinForms; + using System.ComponentModel; + + namespace Compatibility + { + [ViewModelControlHost("System.Windows.Forms.UserControl")] + public partial class ViewModelHost + { + private IContainer? components; + private void InitializeComponent() + { + } + } + } + """; + + var (compilation, generatedSource) = + RunWinFormsHostGenerator(source, ".ViewModelControlHost.g.cs"); + var disposableRegistrationIndex = generatedSource.IndexOf("_disposables.Add(viewModelSubscription);", StringComparison.Ordinal); + var connectIndex = generatedSource.IndexOf("viewModelSubscription.Connect(", StringComparison.Ordinal); + + await Assert.That(GetErrors(compilation)).IsEmpty(); + await Assert.That(disposableRegistrationIndex).IsGreaterThanOrEqualTo(0); + await Assert.That(connectIndex).IsGreaterThan(disposableRegistrationIndex); + await Assert.That(generatedSource.Contains("private readonly DisposableCollection _disposables = new();", StringComparison.Ordinal)).IsTrue(); + await Assert.That(AreCombineLatestCallbacksSerialized(generatedSource)).IsTrue(); + } + + /// Runs a Windows Forms host generator and returns its generated host source. + /// The Windows Forms host generator type. + /// The consumer source text. + /// The suffix identifying the generated host source. + /// The output compilation and generated host source. + private static (Compilation Compilation, string GeneratedSource) RunWinFormsHostGenerator( + string source, + string generatedHintSuffix) + where TGenerator : IIncrementalGenerator, new() + { + var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + var compilation = CSharpCompilation.Create( + "WinFormsHostConsumer", + [ + CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8), parseOptions), + CSharpSyntaxTree.ParseText( + SourceText.From(TestCompilationReferences.WindowsDesktopStubs, Encoding.UTF8), + parseOptions, + path: "WindowsDesktopStubs.g.cs"), + ], + TestCompilationReferences.CreatePortableDefault(), + new(OutputKind.DynamicallyLinkedLibrary)); + GeneratorDriver driver = CSharpGeneratorDriver + .Create([new TGenerator()]) + .WithUpdatedParseOptions(parseOptions); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); + foreach (var generatorResult in driver.GetRunResult().Results) + { + foreach (var generatedSourceResult in generatorResult.GeneratedSources) + { + if (generatedSourceResult.HintName.EndsWith(generatedHintSuffix, StringComparison.Ordinal)) + { + return (outputCompilation, generatedSourceResult.SourceText.ToString()); + } + } + } + + throw new InvalidOperationException("The Windows Forms host generator did not produce source."); + } + + /// Checks that generated CombineLatest callbacks execute within the subscription gate. + /// The generated host source to inspect. + /// when both source callbacks are serialized by a lock. + private static bool AreCombineLatestCallbacksSerialized(string generatedSource) + { + var callbackCount = 0; + foreach (var node in CSharpSyntaxTree.ParseText(generatedSource).GetRoot().DescendantNodes()) + { + if (node is not MethodDeclarationSyntax method + || method.Identifier.ValueText is not ("SetLeft" or "SetRight")) + { + continue; + } + + callbackCount++; + if (!ContainsLockedOnNext(method)) + { + return false; + } + } + + return callbackCount == CombineLatestCallbackCount; + } + + /// Checks whether a generated callback invokes _onNext inside a lock statement. + /// The generated callback method. + /// when the callback is serialized. + private static bool ContainsLockedOnNext(MethodDeclarationSyntax method) + { + foreach (var node in method.DescendantNodes()) + { + if (node is LockStatementSyntax lockStatement + && lockStatement.Statement.ToString().Contains("_onNext(", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// Runs the Reactive and ReactiveCommand generators against a consumer compilation. + /// The consumer source text. + /// The metadata references for the consumer compilation. + /// The output compilation and generated command source. + private static (Compilation Compilation, string GeneratedSource) RunCommandGenerator( + string source, + ImmutableArray references) + { + var compilation = CSharpCompilation.Create( + "Consumer", + [CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8), CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview))], + references, + new(OutputKind.DynamicallyLinkedLibrary)); + var driver = CSharpGeneratorDriver + .Create([new ReactiveCommandGenerator(), new ReactiveGenerator()]) + .WithUpdatedParseOptions((CSharpParseOptions)compilation.SyntaxTrees.First().Options); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); + string? generatedSource = null; + foreach (var generatorResult in driver.GetRunResult().Results) + { + foreach (var generatedSourceResult in generatorResult.GeneratedSources) + { + if (generatedSourceResult.HintName.EndsWith(".ReactiveCommands.g.cs", StringComparison.Ordinal)) + { + generatedSource = generatedSourceResult.SourceText.ToString(); + break; + } + } + + if (generatedSource is not null) + { + break; + } + } + + return (outputCompilation, generatedSource ?? throw new InvalidOperationException("The command generator did not produce source.")); + } + + /// Creates an in-memory metadata reference implementing the pre-v24 ReactiveUI API. + /// The portable executable metadata reference. + private static PortableExecutableReference CreateLegacyReactiveUiReference() + { + const string source = """ + using System; + using System.ComponentModel; + using System.Reactive; + using System.Reflection; + + [assembly: AssemblyVersion("23.2.28.0")] + + namespace ReactiveUI; + + public interface IReactiveObject : INotifyPropertyChanged, INotifyPropertyChanging + { + void RaisePropertyChanged(PropertyChangedEventArgs args); + void RaisePropertyChanging(PropertyChangingEventArgs args); + } + + public class ReactiveObject : IReactiveObject + { + public event PropertyChangedEventHandler? PropertyChanged; + public event PropertyChangingEventHandler? PropertyChanging; + public void RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + public void RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + } + + public sealed class ReactiveCommand + { + } + + public static class ReactiveCommand + { + public static ReactiveCommand Create(Action execute) => new(); + } + """; + var compilation = CSharpCompilation.Create( + "ReactiveUI", + [CSharpSyntaxTree.ParseText(source)], + TestCompilationReferences.CreateForAssemblies( + typeof(object).Assembly, + typeof(System.ComponentModel.INotifyPropertyChanged).Assembly, + typeof(System.Reactive.Unit).Assembly), + new(OutputKind.DynamicallyLinkedLibrary)); + using var stream = new MemoryStream(); + var result = compilation.Emit(stream); + if (!result.Success) + { + throw new InvalidOperationException(string.Join(Environment.NewLine, result.Diagnostics)); + } + + return MetadataReference.CreateFromImage(stream.ToArray()); + } + + /// Formats all compilation errors for assertion output. + /// The compilation to inspect. + /// The formatted compilation errors. + private static string GetErrors(Compilation compilation) + { + var errors = new List(); + foreach (var diagnostic in compilation.GetDiagnostics()) + { + if (diagnostic.Severity == DiagnosticSeverity.Error) + { + errors.Add(diagnostic.ToString()); + } + } + + return string.Join(Environment.NewLine, errors); + } +} diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RoslynHashCodeTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RoslynHashCodeTests.cs new file mode 100644 index 00000000..95a03ad5 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RoslynHashCodeTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +extern alias RoslynSourceGenerator; + +using RoslynHashCode = RoslynSourceGenerator::System.HashCode; + +namespace ReactiveUI.SourceGenerator.Tests; + +/// Tests the Roslyn-targeted System.HashCode polyfill. +public sealed class RoslynHashCodeTests +{ + /// The first test value. + private const int FirstValue = 1; + + /// The second test value. + private const int SecondValue = 2; + + /// The third test value. + private const int ThirdValue = 3; + + /// The fourth test value. + private const int FourthValue = 4; + + /// The fifth test value. + private const int FifthValue = 5; + + /// The sixth test value. + private const int SixthValue = 6; + + /// The seventh test value. + private const int SeventhValue = 7; + + /// The eighth test value. + private const int EighthValue = 8; + + /// Equivalent sequences produce value-equal hash states. + /// A task to monitor the async. + [Test] + public async Task EquivalentSequences_AreEqualAndProduceEqualHashes() + { + var first = CreateHash(FirstValue, SecondValue, ThirdValue, FourthValue, FifthValue, SixthValue, SeventhValue, EighthValue); + var second = CreateHash(FirstValue, SecondValue, ThirdValue, FourthValue, FifthValue, SixthValue, SeventhValue, EighthValue); + object boxedSecond = second; + + await Assert.That(first.Equals(second)).IsTrue(); + await Assert.That(first.Equals(boxedSecond)).IsTrue(); + await Assert.That(first.Equals(null)).IsFalse(); + await Assert.That(first.GetHashCode()).IsEqualTo(second.GetHashCode()); + await Assert.That(first.ToHashCode()).IsEqualTo(second.ToHashCode()); + } + + /// Queue sizes, null values, and differing states are handled. + /// A task to monitor the async. + [Test] + public async Task DifferentSequences_ExerciseEveryQueuePosition() + { + var empty = CreateHash(); + var one = CreateHash(FirstValue); + var two = CreateHash(FirstValue, SecondValue); + var three = CreateHash(FirstValue, SecondValue, ThirdValue); + var four = CreateHash(FirstValue, SecondValue, ThirdValue, FourthValue); + var withNull = default(RoslynHashCode); + var withZero = default(RoslynHashCode); + + withNull.Add(null); + withZero.Add(0); + + await Assert.That(empty.ToHashCode()).IsEqualTo(empty.GetHashCode()); + await Assert.That(one.ToHashCode()).IsEqualTo(one.GetHashCode()); + await Assert.That(two.ToHashCode()).IsEqualTo(two.GetHashCode()); + await Assert.That(three.ToHashCode()).IsEqualTo(three.GetHashCode()); + await Assert.That(four.ToHashCode()).IsEqualTo(four.GetHashCode()); + await Assert.That(withNull.Equals(withZero)).IsTrue(); + await Assert.That(withNull.ToHashCode()).IsEqualTo(withZero.ToHashCode()); + await Assert.That(four.Equals(CreateHash(FourthValue, ThirdValue, SecondValue, FirstValue))).IsFalse(); + await Assert.That(four.Equals("not a hash state")).IsFalse(); + } + + /// Creates a Roslyn hash state from an ordered sequence. + /// Values to add. + /// The populated hash state. + private static RoslynHashCode CreateHash(params int[] values) + { + var hash = default(RoslynHashCode); + + foreach (var value in values) + { + hash.Add(value); + } + + return hash; + } +} diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs index ef9182b7..e1326156 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for the ReactiveCommand generator covering edge cases. -/// +/// Extended unit tests for the ReactiveCommand generator covering edge cases. public class RxCmdExtTests : TestBase { - /// - /// Tests ReactiveCommand with CanExecute observable property. - /// + /// Tests ReactiveCommand with CanExecute observable property. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithCanExecute() @@ -38,9 +33,7 @@ private void DoWork() { } return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with CanExecute observable method. - /// + /// Tests ReactiveCommand with CanExecute observable method. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithCanExecuteMethod() @@ -65,9 +58,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with CancellationToken parameter. - /// + /// Tests ReactiveCommand with CancellationToken parameter. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithCancellationToken() @@ -94,9 +85,7 @@ private async Task LongRunningOperation(CancellationToken ct) return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommands distributed across partial declarations. - /// + /// Tests ReactiveCommands distributed across partial declarations. /// A task to monitor the async. [Test] public Task FromReactiveCommandsAcrossPartialDeclarations() @@ -129,9 +118,7 @@ private async Task LoadAsync() return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with CancellationToken and parameter. - /// + /// Tests ReactiveCommand with CancellationToken and parameter. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithCancellationTokenAndParameter() @@ -159,9 +146,7 @@ private async Task ProcessWithCancellation(string input, CancellationTok return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand returning ValueTask. - /// + /// Tests ReactiveCommand returning ValueTask. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithValueTask() @@ -193,9 +178,7 @@ private ValueTask GetValueTaskResult() return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with protected access modifier. - /// + /// Tests ReactiveCommand with protected access modifier. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithProtectedAccess() @@ -217,9 +200,7 @@ private void ProtectedCommand() { } return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with private access modifier. - /// + /// Tests ReactiveCommand with private access modifier. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithPrivateAccess() @@ -241,9 +222,7 @@ private void PrivateCommand() { } return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with complex generic return type. - /// + /// Tests ReactiveCommand with complex generic return type. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithComplexGenericReturnType() @@ -277,9 +256,7 @@ private async Task> GetAsyncComplexData() return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with tuple return type. - /// + /// Tests ReactiveCommand with tuple return type. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithTupleReturnType() @@ -312,9 +289,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with multiple parameters. - /// + /// Tests ReactiveCommand with multiple parameters. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithMultipleParameters() @@ -340,9 +315,7 @@ private string CombineValues((int number, string text) input) return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with nullable parameter. - /// + /// Tests ReactiveCommand with nullable parameter. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithNullableParameter() @@ -374,9 +347,7 @@ private int ProcessNullableInt(int? input) return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand in generic class. - /// + /// Tests ReactiveCommand in generic class. /// A task to monitor the async. [Test] public Task FromReactiveCommandInGenericClass() @@ -409,9 +380,7 @@ public partial class GenericVM : ReactiveObject where T : class return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with enum parameter. - /// + /// Tests ReactiveCommand with enum parameter. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithEnumParameter() @@ -441,9 +410,7 @@ private void SetStatus(Status? status) { } return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with array parameter. - /// + /// Tests ReactiveCommand with array parameter. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithArrayParameter() @@ -475,9 +442,7 @@ private string[] ProcessStrings(string[]? input) return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with IObservable return. - /// + /// Tests ReactiveCommand with IObservable return. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithObservableReturn() @@ -503,9 +468,7 @@ private IObservable GetObservableSequence() return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with both CanExecute and OutputScheduler. - /// + /// Tests ReactiveCommand with both CanExecute and OutputScheduler. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithCanExecuteAndScheduler() @@ -535,9 +498,7 @@ private async Task ExecuteWithScheduler() return TestHelper.TestPass(sourceCode); } - /// - /// Tests multiple ReactiveCommands in same class. - /// + /// Tests multiple ReactiveCommands in same class. /// A task to monitor the async. [Test] public Task FromMultipleReactiveCommands() @@ -576,9 +537,7 @@ private void Delete() { } return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand in record class. - /// + /// Tests ReactiveCommand in record class. /// A task to monitor the async. [Test] public Task FromReactiveCommandInRecordClass() @@ -608,9 +567,7 @@ private async Task GetValueAsync() return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with delegate parameter. - /// + /// Tests ReactiveCommand with delegate parameter. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithDelegateParameter() @@ -641,9 +598,7 @@ private int ExecuteFunc(Func? func) return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with Task of nullable return. - /// + /// Tests ReactiveCommand with Task of nullable return. /// A task to monitor the async. [Test] public Task FromReactiveCommandWithTaskOfNullableReturn() @@ -677,9 +632,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCommand with deeply nested class. - /// + /// Tests ReactiveCommand with deeply nested class. /// A task to monitor the async. [Test] public Task FromReactiveCommandInDeeplyNestedClass() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs index d914937f..dd20282e 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs @@ -1,20 +1,15 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerator.Tests; namespace ReactiveUI.SourceGenerators.Tests; -/// -/// Extended unit tests for the ReactiveCollection generator covering edge cases. -/// +/// Extended unit tests for the ReactiveCollection generator covering edge cases. public class RxCollExtTests : TestBase { - /// - /// Tests ReactiveCollection with multiple collections. - /// + /// Tests ReactiveCollection with multiple collections. /// A task to monitor the async. [Test] public Task Multiple() @@ -42,9 +37,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with complex generic type. - /// + /// Tests ReactiveCollection with complex generic type. /// A task to monitor the async. [Test] public Task ComplexType() @@ -72,9 +65,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection in nested class. - /// + /// Tests ReactiveCollection in nested class. /// A task to monitor the async. [Test] public Task Nested() @@ -108,9 +99,7 @@ public partial class DeepInnerVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with generic class. - /// + /// Tests ReactiveCollection with generic class. /// A task to monitor the async. [Test] public Task Generic() @@ -132,9 +121,7 @@ public partial class GenericVM : ReactiveObject where T : class return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with record type. - /// + /// Tests ReactiveCollection with record type. /// A task to monitor the async. [Test] public Task Record() @@ -159,9 +146,7 @@ public partial record TestVMRecord : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with interface element type. - /// + /// Tests ReactiveCollection with interface element type. /// A task to monitor the async. [Test] public Task Interface() @@ -193,9 +178,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with tuple type elements. - /// + /// Tests ReactiveCollection with tuple type elements. /// A task to monitor the async. [Test] public Task Tuple() @@ -217,9 +200,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection in different namespaces. - /// + /// Tests ReactiveCollection in different namespaces. /// A task to monitor the async. [Test] public Task DiffNs() @@ -251,9 +232,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with enum element type. - /// + /// Tests ReactiveCollection with enum element type. /// A task to monitor the async. [Test] public Task Enum() @@ -277,9 +256,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with struct element type. - /// + /// Tests ReactiveCollection with struct element type. /// A task to monitor the async. [Test] public Task Struct() @@ -307,9 +284,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with Guid element type. - /// + /// Tests ReactiveCollection with Guid element type. /// A task to monitor the async. [Test] public Task Guid() @@ -332,9 +307,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with DateTime element type. - /// + /// Tests ReactiveCollection with DateTime element type. /// A task to monitor the async. [Test] public Task DateTime() @@ -360,9 +333,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection combined with reactive properties. - /// + /// Tests ReactiveCollection combined with reactive properties. /// A task to monitor the async. [Test] public Task WithReactive() @@ -393,9 +364,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with nullable element type. - /// + /// Tests ReactiveCollection with nullable element type. /// A task to monitor the async. [Test] public Task Nullable() @@ -420,9 +389,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveCollection with byte array element type. - /// + /// Tests ReactiveCollection with byte array element type. /// A task to monitor the async. [Test] public Task ByteArray() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs index 32c0e5ae..a1abde47 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for the Reactive generator covering edge cases. -/// +/// Extended unit tests for the Reactive generator covering edge cases. public class RxGenExtTests : TestBase { - /// - /// Tests reactive property with generic type parameter. - /// + /// Tests reactive property with generic type parameter. /// A task to monitor the async. [Test] public Task Generic() @@ -41,9 +36,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property in a record class. - /// + /// Tests reactive property in a record class. /// A task to monitor the async. [Test] public Task Record() @@ -68,9 +61,7 @@ public partial record TestVMRecord : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with nullable value types. - /// + /// Tests reactive property with nullable value types. /// A task to monitor the async. [Test] public Task Nullable() @@ -101,9 +92,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with override inheritance modifier. - /// + /// Tests reactive property with override inheritance modifier. /// A task to monitor the async. [Test] public Task Override() @@ -131,9 +120,7 @@ public partial class DerivedVM : BaseVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with new inheritance modifier. - /// + /// Tests reactive property with new inheritance modifier. /// A task to monitor the async. [Test] public Task NewMod() @@ -161,9 +148,7 @@ public partial class DerivedVM : BaseVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests multiple reactive properties with different access modifiers. - /// + /// Tests multiple reactive properties with different access modifiers. /// A task to monitor the async. [Test] public Task MixedAccess() @@ -200,9 +185,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with array types. - /// + /// Tests reactive property with array types. /// A task to monitor the async. [Test] public Task Arrays() @@ -230,9 +213,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with tuple types. - /// + /// Tests reactive property with tuple types. /// A task to monitor the async. [Test] public Task Tuples() @@ -260,9 +241,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with custom struct type. - /// + /// Tests reactive property with custom struct type. /// A task to monitor the async. [Test] public Task Struct() @@ -293,9 +272,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with multiple AlsoNotify properties. - /// + /// Tests reactive property with multiple AlsoNotify properties. /// A task to monitor the async. [Test] public Task MultiAlso() @@ -323,9 +300,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property in deeply nested classes. - /// + /// Tests reactive property in deeply nested classes. /// A task to monitor the async. [Test] public Task DeepNested() @@ -371,9 +346,7 @@ public partial class Level5 : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with enum types. - /// + /// Tests reactive property with enum types. /// A task to monitor the async. [Test] public Task Enums() @@ -406,9 +379,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with interface types. - /// + /// Tests reactive property with interface types. /// A task to monitor the async. [Test] public Task Interfaces() @@ -442,9 +413,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with delegate types. - /// + /// Tests reactive property with delegate types. /// A task to monitor the async. [Test] public Task Delegates() @@ -472,9 +441,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with complex generic constraints. - /// + /// Tests reactive property with complex generic constraints. /// A task to monitor the async. [Test] public Task ComplexGen() @@ -503,9 +470,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property in generic class. - /// + /// Tests reactive property in generic class. /// A task to monitor the async. [Test] public Task GenClass() @@ -541,9 +506,7 @@ public partial class GenericVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with multiple attributes on same field. - /// + /// Tests reactive property with multiple attributes on same field. /// A task to monitor the async. [Test] public Task MultiAttr() @@ -576,9 +539,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive properties in multiple partial class definitions. - /// + /// Tests reactive properties in multiple partial class definitions. /// A task to monitor the async. [Test] public Task MultiPartial() @@ -612,9 +573,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with readonly struct types. - /// + /// Tests reactive property with readonly struct types. /// A task to monitor the async. [Test] public Task ReadOnly() @@ -646,9 +605,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with Lazy types. - /// + /// Tests reactive property with Lazy types. /// A task to monitor the async. [Test] public Task Lazy() @@ -673,9 +630,7 @@ public partial class TestVM : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests reactive property with TimeSpan and DateTimeOffset. - /// + /// Tests reactive property with TimeSpan and DateTimeOffset. /// A task to monitor the async. [Test] public Task Time() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs index ff1c66a9..85fb3943 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for the ReactiveObject generator covering edge cases. -/// +/// Extended unit tests for the ReactiveObject generator covering edge cases. public class RxObjExtTests : TestBase { - /// - /// Tests ReactiveObject with multiple reactive properties. - /// + /// Tests ReactiveObject with multiple reactive properties. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithMultipleProperties() @@ -43,9 +38,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with nested class. - /// + /// Tests ReactiveObject with nested class. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithNestedClass() @@ -74,9 +67,7 @@ public partial class ChildVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with ObservableAsProperty. - /// + /// Tests ReactiveObject with ObservableAsProperty. /// A task to monitor the async. [Test] public Task WithOap() @@ -101,9 +92,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with generic type parameters. - /// + /// Tests ReactiveObject with generic type parameters. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithGenericClass() @@ -128,9 +117,7 @@ public partial class GenericVM where T : class return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with multiple type constraints. - /// + /// Tests ReactiveObject with multiple type constraints. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithMultipleTypeConstraints() @@ -160,9 +147,7 @@ public interface IEntity return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with record. - /// + /// Tests ReactiveObject with record. /// A task to monitor the async. [Test] public Task FromReactiveObjectRecord() @@ -187,9 +172,7 @@ public partial record TestVMRecord return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with access modifiers on reactive properties. - /// + /// Tests ReactiveObject with access modifiers on reactive properties. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithAccessModifiers() @@ -217,9 +200,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with inheritance modifiers. - /// + /// Tests ReactiveObject with inheritance modifiers. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithInheritanceModifiers() @@ -248,9 +229,7 @@ public partial class DerivedVM : BaseVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with complex types. - /// + /// Tests ReactiveObject with complex types. /// A task to monitor the async. [Test] public Task ComplexType() @@ -282,9 +261,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with nullable value types. - /// + /// Tests ReactiveObject with nullable value types. /// A task to monitor the async. [Test] public Task Nullable() @@ -315,9 +292,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with attributes on properties. - /// + /// Tests ReactiveObject with attributes on properties. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithAttributes() @@ -349,9 +324,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with deeply nested classes. - /// + /// Tests ReactiveObject with deeply nested classes. /// A task to monitor the async. [Test] public Task FromReactiveObjectWithDeeplyNestedClasses() @@ -394,9 +367,7 @@ public partial class Level4 return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with enum types. - /// + /// Tests ReactiveObject with enum types. /// A task to monitor the async. [Test] public Task Enums() @@ -429,9 +400,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with interface types. - /// + /// Tests ReactiveObject with interface types. /// A task to monitor the async. [Test] public Task Interface() @@ -465,9 +434,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject with basic properties. - /// + /// Tests ReactiveObject with basic properties. /// A task to monitor the async. [Test] public Task Basic() @@ -492,9 +459,7 @@ public partial class TestVM return TestHelper.TestPass(sourceCode); } - /// - /// Tests ReactiveObject in multiple namespaces. - /// + /// Tests ReactiveObject in multiple namespaces. /// A task to monitor the async. [Test] public Task MultiNs() diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/SymbolExtensionTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/SymbolExtensionTests.cs index e61b3717..b9c619fd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/SymbolExtensionTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/SymbolExtensionTests.cs @@ -1,24 +1,27 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Extensions; namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Unit tests for , -/// , -/// , and -/// . -/// All tests compile small in-memory programs to obtain real Roslyn symbol instances. -/// +/// Unit tests for symbol and compilation extensions using real in-memory Roslyn symbols. public sealed class SymbolExtensionTests { - /// - /// GetFullyQualifiedName returns the global:: prefixed name. - /// + /// The fully qualified metadata name of . + private const string ObsoleteAttributeMetadataName = "System.ObsoleteAttribute"; + + /// The fully qualified metadata name of the test-derived type. + private const string DerivedTypeMetadataName = "T.Derived"; + + /// Source for a public class in the shared test namespace. + private const string PublicClassSource = """ + namespace T; + public class C { } + """; + + /// GetFullyQualifiedName returns the global:: prefixed name. /// A task to monitor the async. [Test] public async Task WhenGetFullyQualifiedNameCalledThenReturnsGlobalPrefixedName() @@ -35,9 +38,7 @@ public class MyClass { } await Assert.That(name).IsEqualTo("global::Foo.Bar.MyClass"); } - /// - /// GetFullyQualifiedNameWithNullabilityAnnotations includes the nullable annotation marker. - /// + /// GetFullyQualifiedNameWithNullabilityAnnotations includes the nullable annotation marker. /// A task to monitor the async. [Test] public async Task WhenGetFullyQualifiedNameWithNullabilityCalledThenIncludesAnnotation() @@ -58,9 +59,7 @@ public class C await Assert.That(name).IsEqualTo("string?"); } - /// - /// HasAttributeWithFullyQualifiedMetadataName returns true when the attribute is present. - /// + /// HasAttributeWithFullyQualifiedMetadataName returns true when the attribute is present. /// A task to monitor the async. [Test] public async Task WhenAttributePresentThenHasAttributeWithNameReturnsTrue() @@ -74,33 +73,29 @@ public class C { } """, "C"); - var result = symbol.HasAttributeWithFullyQualifiedMetadataName("System.ObsoleteAttribute"); + var result = symbol.HasAttributeWithFullyQualifiedMetadataName(ObsoleteAttributeMetadataName); await Assert.That(result).IsTrue(); } - /// - /// HasAttributeWithFullyQualifiedMetadataName returns false when the attribute is absent. - /// + /// HasAttributeWithFullyQualifiedMetadataName returns false when the attribute is absent. /// A task to monitor the async. [Test] public async Task WhenAttributeAbsentThenHasAttributeWithNameReturnsFalse() { var symbol = GetTypeSymbol( """ - namespace T; + namespace AttributeFreeTest; public class C { } """, "C"); - var result = symbol.HasAttributeWithFullyQualifiedMetadataName("System.ObsoleteAttribute"); + var result = symbol.HasAttributeWithFullyQualifiedMetadataName(ObsoleteAttributeMetadataName); await Assert.That(result).IsFalse(); } - /// - /// TryGetAttributeWithFullyQualifiedMetadataName returns true and outputs AttributeData when present. - /// + /// TryGetAttributeWithFullyQualifiedMetadataName returns true and outputs AttributeData when present. /// A task to monitor the async. [Test] public async Task WhenAttributePresentThenTryGetAttributeSucceeds() @@ -115,57 +110,46 @@ public class C { } "C"); var found = symbol.TryGetAttributeWithFullyQualifiedMetadataName( - "System.ObsoleteAttribute", + ObsoleteAttributeMetadataName, out var attributeData); await Assert.That(found).IsTrue(); await Assert.That(attributeData).IsNotNull(); } - /// - /// TryGetAttributeWithFullyQualifiedMetadataName returns false when attribute is absent. - /// + /// TryGetAttributeWithFullyQualifiedMetadataName returns false when attribute is absent. /// A task to monitor the async. [Test] public async Task WhenAttributeAbsentThenTryGetAttributeFails() { var symbol = GetTypeSymbol( """ - namespace T; + namespace AttributeLookupTest; public class C { } """, "C"); var found = symbol.TryGetAttributeWithFullyQualifiedMetadataName( - "System.ObsoleteAttribute", + ObsoleteAttributeMetadataName, out var attributeData); await Assert.That(found).IsFalse(); await Assert.That(attributeData).IsNull(); } - /// - /// GetEffectiveAccessibility returns Public for a public class. - /// + /// GetEffectiveAccessibility returns Public for a public class. /// A task to monitor the async. [Test] public async Task WhenPublicClassThenEffectiveAccessibilityIsPublic() { - var symbol = GetTypeSymbol( - """ - namespace T; - public class C { } - """, - "C"); + var symbol = GetTypeSymbol(PublicClassSource, "C"); var accessibility = symbol.GetEffectiveAccessibility(); await Assert.That(accessibility).IsEqualTo(Accessibility.Public); } - /// - /// GetEffectiveAccessibility returns Internal for an internal class. - /// + /// GetEffectiveAccessibility returns Internal for an internal class. /// A task to monitor the async. [Test] public async Task WhenInternalClassThenEffectiveAccessibilityIsInternal() @@ -182,26 +166,17 @@ internal class C { } await Assert.That(accessibility).IsEqualTo(Accessibility.Internal); } - /// - /// GetAccessibilityString returns "public" for a public symbol. - /// + /// GetAccessibilityString returns "public" for a public symbol. /// A task to monitor the async. [Test] public async Task WhenPublicThenGetAccessibilityStringReturnsPublic() { - var symbol = GetTypeSymbol( - """ - namespace T; - public class C { } - """, - "C"); + var symbol = GetTypeSymbol(PublicClassSource, "C"); await Assert.That(symbol.GetAccessibilityString()).IsEqualTo("public"); } - /// - /// GetAccessibilityString returns "internal" for an internal symbol. - /// + /// GetAccessibilityString returns "internal" for an internal symbol. /// A task to monitor the async. [Test] public async Task WhenInternalThenGetAccessibilityStringReturnsInternal() @@ -216,28 +191,19 @@ internal class C { } await Assert.That(symbol.GetAccessibilityString()).IsEqualTo("internal"); } - /// - /// HasOrInheritsFromFullyQualifiedMetadataName returns true for the type itself. - /// + /// HasOrInheritsFromFullyQualifiedMetadataName returns true for the type itself. /// A task to monitor the async. [Test] public async Task WhenTypeIsSelfThenHasOrInheritsReturnsTrue() { - var symbol = GetTypeSymbol( - """ - namespace T; - public class C { } - """, - "C"); + var symbol = GetTypeSymbol(PublicClassSource, "C"); var result = symbol.HasOrInheritsFromFullyQualifiedMetadataName("T.C"); await Assert.That(result).IsTrue(); } - /// - /// HasOrInheritsFromFullyQualifiedMetadataName returns true for a direct base class. - /// + /// HasOrInheritsFromFullyQualifiedMetadataName returns true for a direct base class. /// A task to monitor the async. [Test] public async Task WhenTypeDerivedFromBaseThenHasOrInheritsReturnsTrue() @@ -248,15 +214,13 @@ public class Base { } public class Derived : Base { } """); - var derived = compilation.GetTypeByMetadataName("T.Derived")!; + var derived = compilation.GetTypeByMetadataName(DerivedTypeMetadataName)!; var result = derived.HasOrInheritsFromFullyQualifiedMetadataName("T.Base"); await Assert.That(result).IsTrue(); } - /// - /// HasOrInheritsFromFullyQualifiedMetadataName returns false for an unrelated type. - /// + /// HasOrInheritsFromFullyQualifiedMetadataName returns false for an unrelated type. /// A task to monitor the async. [Test] public async Task WhenTypeUnrelatedThenHasOrInheritsReturnsFalse() @@ -273,28 +237,19 @@ public class B { } await Assert.That(result).IsFalse(); } - /// - /// InheritsFromFullyQualifiedMetadataName returns false for the type itself (not inherited). - /// + /// InheritsFromFullyQualifiedMetadataName returns false for the type itself (not inherited). /// A task to monitor the async. [Test] public async Task WhenTypeSelfThenInheritsReturnsFalse() { - var symbol = GetTypeSymbol( - """ - namespace T; - public class C { } - """, - "C"); + var symbol = GetTypeSymbol(PublicClassSource, "C"); var result = symbol.InheritsFromFullyQualifiedMetadataName("T.C"); await Assert.That(result).IsFalse(); } - /// - /// ImplementsFullyQualifiedMetadataName returns true when the interface is implemented. - /// + /// ImplementsFullyQualifiedMetadataName returns true when the interface is implemented. /// A task to monitor the async. [Test] public async Task WhenInterfaceImplementedThenImplementsReturnsTrue() @@ -311,9 +266,7 @@ public class C : IFoo { } await Assert.That(result).IsTrue(); } - /// - /// GetFullyQualifiedMetadataName returns dotted name without global:: prefix. - /// + /// GetFullyQualifiedMetadataName returns dotted name without global:: prefix. /// A task to monitor the async. [Test] public async Task WhenGetFullyQualifiedMetadataNameCalledThenReturnsDottedName() @@ -330,9 +283,7 @@ public class Baz { } await Assert.That(name).IsEqualTo("Foo.Bar.Baz"); } - /// - /// GetAllMembers returns members from both the type and its base types. - /// + /// GetAllMembers returns members from both the type and its base types. /// A task to monitor the async. [Test] public async Task WhenGetAllMembersCalledThenIncludesInheritedMembers() @@ -349,16 +300,18 @@ public class Derived : Base } """); - var derived = (INamedTypeSymbol)compilation.GetTypeByMetadataName("T.Derived")!; - var members = derived.GetAllMembers().Select(m => m.Name).ToList(); + var derived = (INamedTypeSymbol)compilation.GetTypeByMetadataName(DerivedTypeMetadataName)!; + var members = new List(); + foreach (var member in derived.GetAllMembers()) + { + members.Add(member.Name); + } await Assert.That(members.Contains("DerivedField")).IsTrue(); await Assert.That(members.Contains("BaseField")).IsTrue(); } - /// - /// GetAllMembers(name) returns members with the matching name from base types. - /// + /// GetAllMembers(name) returns members with the matching name from base types. /// A task to monitor the async. [Test] public async Task WhenGetAllMembersWithNameCalledThenFiltersCorrectly() @@ -375,33 +328,24 @@ public class Derived : Base } """); - var derived = (INamedTypeSymbol)compilation.GetTypeByMetadataName("T.Derived")!; - var members = derived.GetAllMembers("Shared").ToList(); + var derived = (INamedTypeSymbol)compilation.GetTypeByMetadataName(DerivedTypeMetadataName)!; + List members = [.. derived.GetAllMembers("Shared")]; await Assert.That(members.Count).IsEqualTo(1); await Assert.That(members[0].Name).IsEqualTo("Shared"); } - /// - /// GetTypeString returns "class" for a regular class. - /// + /// GetTypeString returns "class" for a regular class. /// A task to monitor the async. [Test] public async Task WhenRegularClassThenGetTypeStringReturnsClass() { - var symbol = (INamedTypeSymbol)GetTypeSymbol( - """ - namespace T; - public class C { } - """, - "C"); + var symbol = (INamedTypeSymbol)GetTypeSymbol(PublicClassSource, "C"); await Assert.That(symbol.GetTypeString()).IsEqualTo("class"); } - /// - /// GetTypeString returns "record" for a record class. - /// + /// GetTypeString returns "record" for a record class. /// A task to monitor the async. [Test] public async Task WhenRecordClassThenGetTypeStringReturnsRecord() @@ -416,9 +360,7 @@ public record C { } await Assert.That(symbol.GetTypeString()).IsEqualTo("record"); } - /// - /// GetTypeString returns "struct" for a regular struct. - /// + /// GetTypeString returns "struct" for a regular struct. /// A task to monitor the async. [Test] public async Task WhenStructThenGetTypeStringReturnsStruct() @@ -433,9 +375,7 @@ public struct S { } await Assert.That(symbol.GetTypeString()).IsEqualTo("struct"); } - /// - /// GetTypeString returns "record struct" for a record struct. - /// + /// GetTypeString returns "record struct" for a record struct. /// A task to monitor the async. [Test] public async Task WhenRecordStructThenGetTypeStringReturnsRecordStruct() @@ -450,9 +390,7 @@ public record struct RS { } await Assert.That(symbol.GetTypeString()).IsEqualTo("record struct"); } - /// - /// GetTypeString returns "interface" for an interface. - /// + /// GetTypeString returns "interface" for an interface. /// A task to monitor the async. [Test] public async Task WhenInterfaceThenGetTypeStringReturnsInterface() @@ -467,9 +405,7 @@ public interface IFoo { } await Assert.That(symbol.GetTypeString()).IsEqualTo("interface"); } - /// - /// HasAccessibleTypeWithMetadataName returns true for System.String (always accessible). - /// + /// HasAccessibleTypeWithMetadataName returns true for System.String (always accessible). /// A task to monitor the async. [Test] public async Task WhenWellKnownTypeThenHasAccessibleTypeReturnsTrue() @@ -481,9 +417,7 @@ public async Task WhenWellKnownTypeThenHasAccessibleTypeReturnsTrue() await Assert.That(result).IsTrue(); } - /// - /// HasAccessibleTypeWithMetadataName returns false for a type that doesn't exist. - /// + /// HasAccessibleTypeWithMetadataName returns false for a type that doesn't exist. /// A task to monitor the async. [Test] public async Task WhenUnknownTypeThenHasAccessibleTypeReturnsFalse() @@ -495,22 +429,45 @@ public async Task WhenUnknownTypeThenHasAccessibleTypeReturnsFalse() await Assert.That(result).IsFalse(); } + /// Finds the single type symbol with the requested name. + /// The source text containing the type. + /// The type name to find. + /// The matching type symbol. private static ITypeSymbol GetTypeSymbol(string source, string typeName) { var compilation = CreateCompilation(source); - return compilation.GetSymbolsWithName(typeName, SymbolFilter.Type) - .OfType() - .Single(); + foreach (var symbol in compilation.GetSymbolsWithName(typeName, SymbolFilter.Type)) + { + if (symbol is ITypeSymbol typeSymbol) + { + return typeSymbol; + } + } + + throw new InvalidOperationException($"Type '{typeName}' was not found."); } + /// Finds the single field symbol with the requested name. + /// The source text containing the field. + /// The field name to find. + /// The matching field symbol. private static IFieldSymbol GetFieldSymbol(string source, string fieldName) { var compilation = CreateCompilation(source); - return compilation.GetSymbolsWithName(fieldName, SymbolFilter.Member) - .OfType() - .Single(); + foreach (var symbol in compilation.GetSymbolsWithName(fieldName, SymbolFilter.Member)) + { + if (symbol is IFieldSymbol fieldSymbol) + { + return fieldSymbol; + } + } + + throw new InvalidOperationException($"Field '{fieldName}' was not found."); } + /// Creates an in-memory compilation for a symbol extension test source. + /// The source text to compile. + /// The created compilation. private static CSharpCompilation CreateCompilation(string source) { var syntaxTree = CSharpSyntaxTree.ParseText( @@ -521,6 +478,6 @@ private static CSharpCompilation CreateCompilation(string source) assemblyName: "SymbolExtTests", syntaxTrees: [syntaxTree], references: TestCompilationReferences.CreateDefault(), - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + options: new(OutputKind.DynamicallyLinkedLibrary)); } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs index fa7a5fa8..280e05ef 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Reflection; @@ -8,6 +7,7 @@ namespace ReactiveUI.SourceGenerator.Tests; +/// Creates metadata-reference sets for in-memory test compilations. internal static class TestCompilationReferences { /// @@ -44,41 +44,119 @@ public class Page : System.Windows.FrameworkElement { } } namespace System.Windows.Forms { - public class Control { } + public enum DockStyle + { + None, + Fill, + } + + public class Control : global::System.ComponentModel.Component + { + public ControlCollection Controls { get; } = new(); + public DockStyle Dock { get; set; } + public void SuspendLayout() { } + public void ResumeLayout() { } + } + + public sealed class ControlCollection : global::System.Collections.Generic.IEnumerable + { + private readonly global::System.Collections.Generic.List controls = new(); + + public int Count => controls.Count; + public void Add(Control control) => controls.Add(control); + public void Clear() => controls.Clear(); + public void Remove(Control? control) + { + if (control is not null) + { + controls.Remove(control); + } + } + + public global::System.Collections.Generic.IEnumerator GetEnumerator() => controls.GetEnumerator(); + global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + public class Form : Control { } public class UserControl : Control { } } """; - // Cache the default references so that the expensive assembly-scanning/file-I/O is only - // performed once per process, not on every test invocation. + /// The shared-framework directory name for Windows desktop assemblies. + private const string WindowsDesktopAppDirectoryName = "Microsoft.WindowsDesktop.App"; + + /// + /// Cache the default references so that the expensive assembly-scanning/file-I/O is only + /// performed once per process, not on every test invocation. + /// private static readonly Lazy> defaultReferences = - new(CreateDefaultCore, LazyThreadSafetyMode.ExecutionAndPublication); + new( + static () => CreateDefaultCore(includeWindowsDesktop: true), + LazyThreadSafetyMode.ExecutionAndPublication); + + /// + /// Cache the platform-neutral references used with source stubs for deterministic + /// Windows desktop generator tests on every operating system. + /// + private static readonly Lazy> portableDefaultReferences = + new( + static () => CreateDefaultCore(includeWindowsDesktop: false), + LazyThreadSafetyMode.ExecutionAndPublication); /// /// Returns metadata references for all assemblies required by the in-memory test compilations. - /// Uses only runtime assemblies already loaded into the current process — no NuGet downloads, - /// no Basic.Reference.Assemblies mixing — to avoid CS1704/CS0433/CS0518 duplicate-type errors. - /// The result is cached after the first call to avoid repeated assembly scanning and file I/O. + /// Uses an explicit transitive closure rooted at the assemblies required by the test sources. + /// It deliberately does not sweep all loaded assemblies, so compatibility tests can model a + /// ReactiveUI 24 base application without accidentally importing System.Reactive. /// + /// The cached default metadata-reference closure. internal static ImmutableArray CreateDefault() => defaultReferences.Value; - private static ImmutableArray CreateDefaultCore() + /// + /// Returns the default metadata-reference closure without platform-specific Windows + /// desktop assemblies so source stubs can be used consistently on every operating system. + /// + /// The cached platform-neutral metadata-reference closure. + internal static ImmutableArray CreatePortableDefault() => portableDefaultReferences.Value; + + /// Creates an isolated transitive metadata-reference closure from the supplied assembly roots. + /// The assemblies whose dependency closures should be included. + /// The isolated metadata references. + internal static ImmutableArray CreateForAssemblies(params Assembly[] assemblies) { var visited = new HashSet(StringComparer.OrdinalIgnoreCase); var result = ImmutableArray.CreateBuilder(); + foreach (var assembly in assemblies) + { + AddTransitive(assembly, visited, result); + } - // Seed with the key assemblies whose transitive closure covers BCL + ReactiveUI + Splat + - // System.Reactive and everything else the test source strings depend on. + return result.ToImmutable(); + } + + /// Builds the default metadata-reference closure. + /// The default metadata-reference closure. + /// + /// Whether to add installed Windows desktop shared-framework assemblies on Windows. + /// + private static ImmutableArray CreateDefaultCore(bool includeWindowsDesktop) + { + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = ImmutableArray.CreateBuilder(); + + // Seed with the key assemblies whose transitive closure covers the dependencies used by + // the existing generator snapshots. Profile-specific tests supply their own references. var seeds = new[] { - typeof(object).Assembly, // System.Private.CoreLib - typeof(Enumerable).Assembly, // System.Linq - typeof(System.ComponentModel.INotifyPropertyChanged).Assembly, // System.ObjectModel - typeof(ReactiveUI.ReactiveObject).Assembly, // ReactiveUI - typeof(ReactiveUI.SourceGenerators.ReactiveGenerator).Assembly, // ReactiveUI.SourceGenerators - typeof(ReactiveUI.SourceGenerators.CodeFixers.PropertyToReactiveFieldAnalyzer).Assembly, // analyzer assembly - typeof(Splat.Locator).Assembly, // Splat + typeof(object).Assembly, // System.Private.CoreLib + typeof(Enumerable).Assembly, // System.Linq + typeof(System.ComponentModel.INotifyPropertyChanged).Assembly, // System.ObjectModel + typeof(ReactiveObject).Assembly, // ReactiveUI + typeof(System.Reactive.Unit).Assembly, // System.Reactive test inputs + typeof(DynamicData.SourceList<>).Assembly, // Bindable derived list test inputs + typeof(ReactiveGenerator).Assembly, // ReactiveUI.SourceGenerators + typeof(PropertyToReactiveFieldAnalyzer).Assembly, // analyzer assembly + typeof(Splat.Locator).Assembly, // Splat }; foreach (var seed in seeds) @@ -86,19 +164,9 @@ private static ImmutableArray CreateDefaultCore() AddTransitive(seed, visited, result); } - // Also sweep all assemblies already loaded — catches System.Reactive, DynamicData, etc. - foreach (var loaded in AppDomain.CurrentDomain.GetAssemblies()) - { - if (!loaded.IsDynamic && !string.IsNullOrWhiteSpace(loaded.Location) - && visited.Add(loaded.Location)) - { - result.Add(MetadataReference.CreateFromFile(loaded.Location)); - } - } - // Add WPF and WinForms assemblies on Windows so test source strings that inherit from // Window or use Windows Forms controls compile correctly. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (includeWindowsDesktop && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { AddWindowsDesktopAssemblies(visited, result); } @@ -111,6 +179,8 @@ private static ImmutableArray CreateDefaultCore() /// reference set, resolving them from the Microsoft.WindowsDesktop.App shared framework /// directory that corresponds to the current runtime version. /// + /// The set of metadata-reference paths already added. + /// The metadata-reference collection to populate. private static void AddWindowsDesktopAssemblies( HashSet visited, ImmutableArray.Builder result) @@ -128,6 +198,7 @@ private static void AddWindowsDesktopAssemblies( "PresentationCore.dll", "WindowsBase.dll", "System.Xaml.dll", + "System.Private.Windows.Core.dll", }; // WinForms assemblies required for tests that use Windows Forms controls. @@ -137,7 +208,22 @@ private static void AddWindowsDesktopAssemblies( "System.Windows.Forms.Primitives.dll", }; - foreach (var name in wpfAssemblies.Concat(winFormsAssemblies)) + AddWindowsDesktopAssemblies(versionDir, wpfAssemblies, visited, result); + AddWindowsDesktopAssemblies(versionDir, winFormsAssemblies, visited, result); + } + + /// Adds Windows desktop assembly references from a shared-framework directory. + /// The Windows desktop shared-framework version directory. + /// The assembly file names to add. + /// The set of metadata-reference paths already added. + /// The metadata-reference collection to populate. + private static void AddWindowsDesktopAssemblies( + string versionDir, + IEnumerable assemblyNames, + HashSet visited, + ImmutableArray.Builder result) + { + foreach (var name in assemblyNames) { var path = Path.Combine(versionDir, name); if (File.Exists(path) && visited.Add(path)) @@ -152,42 +238,22 @@ private static void AddWindowsDesktopAssemblies( /// Uses multiple discovery strategies: runtime-relative path, DOTNET_ROOT env var, /// and well-known installation paths. /// + /// The directory that contains the best matching Windows desktop reference assemblies, if found. private static string? FindWindowsDesktopAppVersionDir() { var runtimeVersion = Environment.Version; var majorMinor = $"{runtimeVersion.Major}.{runtimeVersion.Minor}"; - // Collect unique candidate parent directories to try, in priority order. - var candidateRoots = new List(); - - // Strategy 1a: RuntimeEnvironment.GetRuntimeDirectory() — the most reliable way to - // locate the actual .NET shared framework even when running under a VS test host - // or PowerShell where typeof(object).Assembly.Location may point elsewhere. - // Returns e.g. C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.14\ - var runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); - candidateRoots.Add(Path.GetDirectoryName(runtimeDir?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); - - // Strategy 1b: Walk up from typeof(object).Assembly.Location (works under dotnet CLI). - var coreLibDir = Path.GetDirectoryName(typeof(object).Assembly.Location); - candidateRoots.Add(coreLibDir); - - // Try each root two levels up to reach shared\Microsoft.WindowsDesktop.App - foreach (var root in candidateRoots.Where(r => !string.IsNullOrEmpty(r))) + var runtimeDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + var candidateRoots = new[] { - var candidate = Path.GetFullPath(Path.Combine(root!, "..", "Microsoft.WindowsDesktop.App")); - var dir = PickBestVersionDir(candidate, majorMinor); - if (dir is not null) - { - return dir; - } - - // One extra level for layouts where root is already the version directory. - candidate = Path.GetFullPath(Path.Combine(root!, "..", "..", "Microsoft.WindowsDesktop.App")); - dir = PickBestVersionDir(candidate, majorMinor); - if (dir is not null) - { - return dir; - } + Path.GetDirectoryName(runtimeDirectory?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), + Path.GetDirectoryName(typeof(object).Assembly.Location), + }; + var runtimeRelativeDirectory = FindWindowsDesktopAppVersionDir(candidateRoots, majorMinor); + if (runtimeRelativeDirectory is not null) + { + return runtimeRelativeDirectory; } // Strategy 2: DOTNET_ROOT environment variable. @@ -195,7 +261,7 @@ private static void AddWindowsDesktopAssemblies( ?? Environment.GetEnvironmentVariable("DOTNET_ROOT(x64)"); if (!string.IsNullOrEmpty(dotnetRoot)) { - var candidate = Path.Combine(dotnetRoot, "shared", "Microsoft.WindowsDesktop.App"); + var candidate = Path.Combine(dotnetRoot, "shared", WindowsDesktopAppDirectoryName); var dir = PickBestVersionDir(candidate, majorMinor); if (dir is not null) { @@ -215,7 +281,7 @@ private static void AddWindowsDesktopAssemblies( continue; } - var candidate = Path.Combine(programFiles, "dotnet", "shared", "Microsoft.WindowsDesktop.App"); + var candidate = Path.Combine(programFiles, "dotnet", "shared", WindowsDesktopAppDirectoryName); var dir = PickBestVersionDir(candidate, majorMinor); if (dir is not null) { @@ -226,10 +292,41 @@ private static void AddWindowsDesktopAssemblies( return null; } - /// - /// Returns the best version directory under that matches - /// (e.g., "9.0"), falling back to the newest available. - /// + /// Searches candidate runtime roots for the best Windows desktop version directory. + /// The runtime-relative candidate roots to inspect. + /// The runtime major and minor version to prefer. + /// The best matching Windows desktop directory, if found. + private static string? FindWindowsDesktopAppVersionDir(IEnumerable candidateRoots, string majorMinor) + { + foreach (var root in candidateRoots) + { + if (string.IsNullOrEmpty(root)) + { + continue; + } + + var candidate = Path.GetFullPath(Path.Combine(root, "..", WindowsDesktopAppDirectoryName)); + var directory = PickBestVersionDir(candidate, majorMinor); + if (directory is not null) + { + return directory; + } + + candidate = Path.GetFullPath(Path.Combine(root, "..", "..", WindowsDesktopAppDirectoryName)); + directory = PickBestVersionDir(candidate, majorMinor); + if (directory is not null) + { + return directory; + } + } + + return null; + } + + /// Returns the best version directory under that matches (e.g., "9.0"), falling back to the newest available. + /// The Windows desktop shared-framework directory. + /// The runtime major and minor version to prefer. + /// The best matching version directory, if it contains WPF assemblies. private static string? PickBestVersionDir(string sharedRoot, string majorMinor) { if (!Directory.Exists(sharedRoot)) @@ -243,20 +340,29 @@ private static void AddWindowsDesktopAssemblies( return null; } - // Prefer exact major.minor match, ordered descending (newest patch first). - var best = dirs - .Where(d => Path.GetFileName(d).StartsWith(majorMinor + ".", StringComparison.Ordinal) - || Path.GetFileName(d).Equals(majorMinor, StringComparison.Ordinal)) - .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase) - .FirstOrDefault() - ?? dirs.OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase).FirstOrDefault(); + Array.Sort(dirs, StringComparer.OrdinalIgnoreCase); + Array.Reverse(dirs); + var best = dirs[0]; + foreach (var directory in dirs) + { + var version = Path.GetFileName(directory); + if (version.StartsWith($"{majorMinor}.", StringComparison.Ordinal) || version.Equals(majorMinor, StringComparison.Ordinal)) + { + best = directory; + break; + } + } // Validate it actually contains PresentationFramework.dll - return best is not null && File.Exists(Path.Combine(best, "PresentationFramework.dll")) + return File.Exists(Path.Combine(best, "PresentationFramework.dll")) ? best : null; } + /// Adds an assembly and its loadable transitive references to the collection. + /// The assembly whose references should be added. + /// The set of assembly paths that have already been added. + /// The metadata-reference collection to populate. private static void AddTransitive( Assembly assembly, HashSet visited, @@ -281,7 +387,15 @@ private static void AddTransitive( var referenced = System.Reflection.Assembly.Load(referencedName); AddTransitive(referenced, visited, result); } - catch + catch (FileNotFoundException) + { + // Best-effort — system assemblies not found in some environments are skipped. + } + catch (FileLoadException) + { + // Best-effort — system assemblies not found in some environments are skipped. + } + catch (BadImageFormatException) { // Best-effort — system assemblies not found in some environments are skipped. } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TypedConstantInfoTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TypedConstantInfoTests.cs new file mode 100644 index 00000000..2eb02a67 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TypedConstantInfoTests.cs @@ -0,0 +1,296 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Helpers; + +namespace ReactiveUI.SourceGenerator.Tests; + +/// Tests serializable typed-constant models and their Roslyn factories. +public sealed class TypedConstantInfoTests +{ + /// The fully qualified Int32 type name. + private const string Int32TypeName = "global::System.Int32"; + + /// A representative character value. + private const char CharacterValue = 'x'; + + /// A representative double value. + private const double DoubleValue = 1.25D; + + /// A representative single value. + private const float SingleValue = 2.5F; + + /// A representative integer value. + private const int IntegerValue = 3; + + /// A representative long value. + private const long LongValue = 4L; + + /// A representative signed-byte value. + private const sbyte SignedByteValue = -5; + + /// A representative short value. + private const short ShortValue = -6; + + /// A representative unsigned-integer value. + private const uint UnsignedIntegerValue = 7U; + + /// A representative unsigned-long value. + private const ulong UnsignedLongValue = 8UL; + + /// A representative unsigned-short value. + private const ushort UnsignedShortValue = 9; + + /// The first array value. + private const int FirstArrayValue = 10; + + /// The second array value. + private const int SecondArrayValue = 11; + + /// Source containing an attribute with every legal constructor constant kind. + private const string AttributeConstantSource = """ + using System; + + namespace Test; + + public enum Choice + { + Negative = -1 + } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class ValuesAttribute : Attribute + { + public ValuesAttribute( + string? nullValue, + string text, + bool flag, + byte byteValue, + char characterValue, + double doubleValue, + float singleValue, + int integerValue, + long longValue, + short shortValue, + Type typeValue, + Choice enumValue, + int[] arrayValue) + { + } + } + + [Values( + null, + "text", + true, + (byte)1, + 'x', + 1.25D, + 2.5F, + 3, + 4L, + (short)-6, + typeof(string), + Choice.Negative, + new[] { 10, 11 })] + public sealed class Target + { + } + """; + + /// All supported constant models produce the expected syntax shapes. + /// A task to monitor the async. + [Test] + public async Task GetSyntax_WithSupportedConstantModels_ProducesValidExpressions() + { + TypedConstantInfo[] values = + [ + new TypedConstantInfo.Primitive.String("text"), + new TypedConstantInfo.Primitive.Boolean(true), + new TypedConstantInfo.Primitive.Boolean(false), + new TypedConstantInfo.Primitive.Of(1), + new TypedConstantInfo.Primitive.Of(CharacterValue), + new TypedConstantInfo.Primitive.Of(DoubleValue), + new TypedConstantInfo.Primitive.Of(SingleValue), + new TypedConstantInfo.Primitive.Of(IntegerValue), + new TypedConstantInfo.Primitive.Of(LongValue), + new TypedConstantInfo.Primitive.Of(SignedByteValue), + new TypedConstantInfo.Primitive.Of(ShortValue), + new TypedConstantInfo.Primitive.Of(UnsignedIntegerValue), + new TypedConstantInfo.Primitive.Of(UnsignedLongValue), + new TypedConstantInfo.Primitive.Of(UnsignedShortValue), + new TypedConstantInfo.Type("global::System.String"), + new TypedConstantInfo.Enum("global::Test.Choice", 1), + new TypedConstantInfo.Enum("global::Test.Choice", -1), + new TypedConstantInfo.Null(), + new TypedConstantInfo.Array( + Int32TypeName, + ImmutableArray.Create( + new TypedConstantInfo.Primitive.Of(FirstArrayValue), + new TypedConstantInfo.Primitive.Of(SecondArrayValue))), + ]; + List expressions = []; + foreach (var value in values) + { + expressions.Add(value.GetSyntax().ToString()); + } + + var allExpressionsHaveText = true; + foreach (var expression in expressions) + { + allExpressionsHaveText &= !string.IsNullOrWhiteSpace(expression); + } + + await Assert.That(expressions.Count).IsEqualTo(values.Length); + await Assert.That(expressions[0]).IsEqualTo("\"text\""); + await Assert.That(expressions[1]).IsEqualTo("true"); + await Assert.That(expressions[2]).IsEqualTo("false"); + await Assert.That(expressions[14]).IsEqualTo("typeof(global::System.String)"); + await Assert.That(expressions[16].Contains("(-1)", StringComparison.Ordinal)).IsTrue(); + await Assert.That(expressions[17]).IsEqualTo("null"); + await Assert.That(expressions[18].Contains(Int32TypeName, StringComparison.Ordinal)).IsTrue(); + await Assert.That(allExpressionsHaveText).IsTrue(); + } + + /// The attribute-data factory supports every legal attribute constant kind. + /// A task to monitor the async. + [Test] + public async Task Create_WithAttributeConstructorConstants_MapsEverySupportedKind() + { + var compilation = CreateCompilation(AttributeConstantSource); + var target = compilation.GetTypeByMetadataName("Test.Target") + ?? throw new InvalidOperationException("The attributed target type was not found."); + var attribute = target.GetAttributes()[0]; + List values = []; + foreach (var argument in attribute.ConstructorArguments) + { + values.Add(TypedConstantInfo.Create(argument)); + } + + await Assert.That(values.Count).IsEqualTo(attribute.ConstructorArguments.Length); + await Assert.That(values[0] is TypedConstantInfo.Null).IsTrue(); + await Assert.That(values[1] is TypedConstantInfo.Primitive.String).IsTrue(); + await Assert.That(values[2] is TypedConstantInfo.Primitive.Boolean).IsTrue(); + await Assert.That(values[10] is TypedConstantInfo.Type).IsTrue(); + await Assert.That(values[11] is TypedConstantInfo.Enum).IsTrue(); + await Assert.That(values[12] is TypedConstantInfo.Array).IsTrue(); + await Assert.That(values[12].GetSyntax().ToString().Contains("10", StringComparison.Ordinal)).IsTrue(); + } + + /// The operation factory handles constants, types, explicit and implicit arrays, and unsupported values. + /// A task to monitor the async. + [Test] + public async Task TryCreate_WithExpressionOperations_CoversSupportedAndUnsupportedShapes() + { + var integer = TryCreateExpression("42"); + var type = TryCreateExpression("typeof(string)"); + var implicitArray = TryCreateExpression("new[] { 1, 2 }"); + var explicitArray = TryCreateExpression("new int[] { 3, 4 }"); + var uninitializedArray = TryCreateExpression("new int[2]"); + var invocation = TryCreateExpression("GetValue()"); + var unsupportedArray = TryCreateExpression("new[] { GetValue() }"); + var allPrimitiveOperationsSucceeded = true; + foreach (var expression in new[] + { + "null", + "\"text\"", + "true", + "(byte)1", + "'x'", + "1.25D", + "2.5F", + "3", + "4L", + "(sbyte)-5", + "(short)-6", + "7U", + "8UL", + "(ushort)9", + }) + { + allPrimitiveOperationsSucceeded &= TryCreateExpression(expression).Success; + } + + await Assert.That(integer.Success).IsTrue(); + await Assert.That(integer.Info?.GetSyntax().ToString()).IsEqualTo("42"); + await Assert.That(type.Success).IsTrue(); + await Assert.That(type.Info is TypedConstantInfo.Type).IsTrue(); + await Assert.That(implicitArray.Success).IsTrue(); + await Assert.That(explicitArray.Success).IsTrue(); + await Assert.That(uninitializedArray.Success).IsTrue(); + await Assert.That(uninitializedArray.Info is TypedConstantInfo.Array).IsTrue(); + await Assert.That((uninitializedArray.Info?.GetSyntax().ToString() ?? string.Empty).Contains("[]", StringComparison.Ordinal)).IsTrue(); + await Assert.That(allPrimitiveOperationsSucceeded).IsTrue(); + await Assert.That(invocation.Success).IsFalse(); + await Assert.That(invocation.Info).IsNull(); + await Assert.That(unsupportedArray.Success).IsFalse(); + await Assert.That(unsupportedArray.Info).IsNull(); + } + + /// Unsupported primitive values report the intended argument errors. + /// A task to monitor the async. + [Test] + public async Task UnsupportedPrimitiveValues_ThrowArgumentException() + { + await Assert.That(static () => new TypedConstantInfo.Primitive.Of(1M).GetSyntax()).Throws(); + await Assert.That(static () => TryCreateExpression("1M")).Throws(); + } + + /// Creates an operation and asks the typed-constant factory to represent it. + /// The expression to compile. + /// The factory result. + private static (bool Success, TypedConstantInfo? Info) TryCreateExpression(string expressionText) + { + var source = $$""" + namespace Test; + + public static class Holder + { + private static int GetValue() => 1; + + private static readonly object? Value = {{expressionText}}; + } + """; + var compilation = CreateCompilation(source); + var syntaxTree = compilation.SyntaxTrees[0]; + var semanticModel = compilation.GetSemanticModel(syntaxTree); + var expression = GetValueExpression(syntaxTree.GetRoot()); + var operation = semanticModel.GetOperation(expression) + ?? throw new InvalidOperationException("The test expression operation was not found."); + var success = TypedConstantInfo.TryCreate(operation, semanticModel, expression, default, out var info); + return (success, info); + } + + /// Finds the value initializer in a test syntax tree. + /// The syntax root to search. + /// The value initializer expression. + private static ExpressionSyntax GetValueExpression(SyntaxNode root) + { + foreach (var node in root.DescendantNodes()) + { + if (node is VariableDeclaratorSyntax { Identifier.ValueText: "Value", Initializer.Value: { } value }) + { + return value; + } + } + + throw new InvalidOperationException("The test expression was not found."); + } + + /// Creates an in-memory compilation for typed-constant tests. + /// The source text to compile. + /// The created compilation. + private static CSharpCompilation CreateCompilation(string source) + { + var syntaxTree = CSharpSyntaxTree.ParseText( + source, + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13)); + return CSharpCompilation.Create( + nameof(TypedConstantInfoTests), + [syntaxTree], + TestCompilationReferences.CreateDefault(), + new(OutputKind.DynamicallyLinkedLibrary)); + } +} diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs index 3e287321..98b5a9bc 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs @@ -1,18 +1,13 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerator.Tests; -/// -/// Extended unit tests for the IViewFor generator covering edge cases. -/// +/// Extended unit tests for the IViewFor generator covering edge cases. public class ViewForExtTests : TestBase { - /// - /// Tests IViewFor with LazySingleton registration type. - /// + /// Tests IViewFor with LazySingleton registration type. /// A task to monitor the async. [Test] public Task LazySingle() @@ -40,9 +35,7 @@ public partial class TestViewModel : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with Constant registration type. - /// + /// Tests IViewFor with Constant registration type. /// A task to monitor the async. [Test] public Task Constant() @@ -70,9 +63,7 @@ public partial class TestViewModel : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with PerRequest registration type. - /// + /// Tests IViewFor with PerRequest registration type. /// A task to monitor the async. [Test] public Task PerReq() @@ -100,9 +91,7 @@ public partial class TestViewModel : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with ViewModel registration. - /// + /// Tests IViewFor with ViewModel registration. /// A task to monitor the async. [Test] public Task FromIViewForWithViewModelRegistration() @@ -132,9 +121,7 @@ public partial class TestViewModel : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with nested ViewModel. - /// + /// Tests IViewFor with nested ViewModel. /// A task to monitor the async. [Test] public Task Nested() @@ -167,9 +154,7 @@ public partial class ChildView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with string-based ViewModel type. - /// + /// Tests IViewFor with string-based ViewModel type. /// A task to monitor the async. [Test] public Task FromIViewForWithStringViewModelType() @@ -197,9 +182,7 @@ public partial class TestViewModel : ReactiveObject return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with different namespaces for View and ViewModel. - /// + /// Tests IViewFor with different namespaces for View and ViewModel. /// A task to monitor the async. [Test] public Task DiffNs() @@ -234,9 +217,7 @@ public partial class ProductView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests multiple IViewFor in same namespace. - /// + /// Tests multiple IViewFor in same namespace. /// A task to monitor the async. [Test] public Task FromMultipleIViewForInSameNamespace() @@ -286,9 +267,7 @@ public partial class View3 : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with generic ViewModel. - /// + /// Tests IViewFor with generic ViewModel. /// A task to monitor the async. [Test] public Task Generic() @@ -316,9 +295,7 @@ public partial class StringItemView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with record ViewModel. - /// + /// Tests IViewFor with record ViewModel. /// A task to monitor the async. [Test] public Task Record() @@ -347,9 +324,7 @@ public partial class RecordView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with deeply nested View class. - /// + /// Tests IViewFor with deeply nested View class. /// A task to monitor the async. [Test] public Task FromIViewForWithNestedViewClass() @@ -383,9 +358,7 @@ public partial class NestedView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with ViewModel having reactive properties. - /// + /// Tests IViewFor with ViewModel having reactive properties. /// A task to monitor the async. [Test] public Task FromIViewForWithReactivePropertiesViewModel() @@ -420,9 +393,7 @@ public partial class ReactivePropertiesView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with ViewModel having ReactiveCommands. - /// + /// Tests IViewFor with ViewModel having ReactiveCommands. /// A task to monitor the async. [Test] public Task FromIViewForWithReactiveCommandsViewModel() @@ -459,9 +430,7 @@ public partial class CommandsView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with all registration options. - /// + /// Tests IViewFor with all registration options. /// A task to monitor the async. [Test] public Task AllRegOpts() @@ -491,9 +460,7 @@ public partial class FullView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with interface ViewModel type via string. - /// + /// Tests IViewFor with interface ViewModel type via string. /// A task to monitor the async. [Test] public Task Interface() @@ -526,9 +493,7 @@ public partial class InterfaceView : Window return TestHelper.TestPass(sourceCode); } - /// - /// Tests IViewFor with ViewModel from external namespace reference. - /// + /// Tests IViewFor with ViewModel from external namespace reference. /// A task to monitor the async. [Test] public Task ExtNs() diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs index 207afcc4..cc790a79 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs @@ -1,39 +1,43 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; namespace ReactiveUI.SourceGenerators.CodeFixers.Extensions; +/// Extension methods for field and property symbols. internal static class FieldSyntaxExtensions { - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - internal static bool IsTargetTypeValid(this IFieldSymbol fieldSymbol) + /// Extension methods for instances. + /// The field symbol to extend. + extension(IFieldSymbol fieldSymbol) { - var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = fieldSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + /// Validates the containing type for a given field being annotated. + /// Whether or not the containing type is valid. + internal bool IsTargetTypeValid() + { + var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); + var isIObservableObject = fieldSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); + var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + } } - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - internal static bool IsTargetTypeValid(this IPropertySymbol propertySymbol) + /// Extension methods for instances. + /// The property symbol to extend. + extension(IPropertySymbol propertySymbol) { - var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = propertySymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + /// Validates the containing type for a given property being annotated. + /// Whether or not the containing type is valid. + internal bool IsTargetTypeValid() + { + var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); + var isIObservableObject = propertySymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); + var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + } } } diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs index 9d11ed19..8508fe77 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs @@ -1,33 +1,32 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; namespace ReactiveUI.SourceGenerators.CodeFixers.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class ISymbolExtensions { - /// - /// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name. - /// - /// The input instance to check. - /// The attribute name to look for. - /// Whether or not has an attribute with the specified name. - public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name) + /// Extension methods for instances. + /// The symbol to extend. + extension(ISymbol symbol) { - foreach (var attribute in symbol.GetAttributes()) + /// Checks whether or not a symbol has an attribute with the specified fully qualified metadata name. + /// The attribute name to look for. + /// Whether the symbol has an attribute with the specified name. + internal bool HasAttributeWithFullyQualifiedMetadataName(string name) { - if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true) + foreach (var attribute in symbol.GetAttributes()) { - return true; + if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true) + { + return true; + } } - } - return false; + return false; + } } } diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs index e2ccb597..2dd198f6 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,130 +8,117 @@ namespace ReactiveUI.SourceGenerators.CodeFixers.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class ITypeSymbolExtensions { - /// - /// Checks whether or not a given inherits from a specified type. - /// - /// The target instance to check. - /// The full name of the type to check for inheritance. - /// Whether or not inherits from . - public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + /// Extension methods for instances. + /// The type symbol to extend. + extension(ITypeSymbol typeSymbol) { - var baseType = typeSymbol.BaseType; - - while (baseType is not null) + /// Checks whether a type symbol inherits from a specified type. + /// The full name of the type to check for inheritance. + /// Whether the type symbol inherits from . + internal bool InheritsFromFullyQualifiedMetadataName(string name) { - if (baseType.HasFullyQualifiedMetadataName(name)) + var baseType = typeSymbol.BaseType; + + while (baseType is not null) { - return true; + if (baseType.HasFullyQualifiedMetadataName(name)) + { + return true; + } + + baseType = baseType.BaseType; } - baseType = baseType.BaseType; + return false; } - return false; - } - - /// - /// Checks whether or not a given implements a specified interface. - /// - /// The target instance to check. - /// The full name of the interface to check for inheritance. - /// Whether or not implements . - public static bool ImplementsFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) - { - foreach (var implementedInterface in typeSymbol.AllInterfaces) + /// Checks whether a type symbol implements a specified interface. + /// The full name of the interface to check for implementation. + /// Whether the type symbol implements . + internal bool ImplementsFullyQualifiedMetadataName(string name) { - if (implementedInterface.HasFullyQualifiedMetadataName(name)) + foreach (var implementedInterface in typeSymbol.AllInterfaces) { - return true; + if (implementedInterface.HasFullyQualifiedMetadataName(name)) + { + return true; + } } - } - return false; - } + return false; + } - /// - /// Checks whether or not a given has or inherits a specified attribute. - /// - /// The target instance to check. - /// The name of the attribute to look for. - /// Whether or not has an attribute with the specified type name. - public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) - { - for (var currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) + /// Checks whether a type symbol has or inherits a specified attribute. + /// The name of the attribute to look for. + /// Whether the type symbol has an attribute with the specified type name. + internal bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(string name) { - if (currentType.HasAttributeWithFullyQualifiedMetadataName(name)) + for (var currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) { - return true; + if (currentType.HasAttributeWithFullyQualifiedMetadataName(name)) + { + return true; + } } - } - return false; - } + return false; + } - /// - /// Checks whether or not a given type symbol has a specified fully qualified metadata name. - /// - /// The input instance to check. - /// The full name to check. - /// Whether has a full name equals to . - public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name) - { - using var builder = ImmutableArrayBuilder.Rent(); + /// Checks whether a type symbol has a specified fully qualified metadata name. + /// The full name to check. + /// Whether the type symbol has a full name equal to . + internal bool HasFullyQualifiedMetadataName(string name) + { + using var builder = ImmutableArrayBuilder.Rent(); - symbol.AppendFullyQualifiedMetadataName(builder); + AppendFullyQualifiedMetadataName(typeSymbol, builder); - return builder.WrittenSpan.StartsWith(name.AsSpan()); + return builder.WrittenSpan.StartsWith(name.AsSpan()); + } } - /// - /// Appends the fully qualified metadata name for a given symbol to a target builder. - /// - /// The input instance. - /// The target instance. - private static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ImmutableArrayBuilder builder) + /// Appends a symbol's fully qualified metadata name to a target builder. + /// The symbol whose metadata name will be appended. + /// The target builder. + private static void AppendFullyQualifiedMetadataName(ITypeSymbol symbol, ImmutableArrayBuilder builder) { - static void BuildFrom(ISymbol? symbol, ImmutableArrayBuilder builder) + static void BuildFrom(ISymbol? current, ImmutableArrayBuilder target) { - switch (symbol) + switch (current) { - // Namespaces that are nested also append a leading '.' case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: - BuildFrom(symbol.ContainingNamespace, builder); - builder.Add('.'); - builder.AddRange(symbol.MetadataName.AsSpan()); - break; + { + BuildFrom(current.ContainingNamespace, target); + target.Add('.'); + target.AddRange(current.MetadataName.AsSpan()); + break; + } - // Other namespaces (ie. the one right before global) skip the leading '.' case INamespaceSymbol { IsGlobalNamespace: false }: - builder.AddRange(symbol.MetadataName.AsSpan()); - break; - - // Types with no namespace just have their metadata name directly written - case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }: - builder.AddRange(symbol.MetadataName.AsSpan()); - break; + case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol } when namespaceSymbol.IsGlobalNamespace: + { + target.AddRange(current.MetadataName.AsSpan()); + break; + } - // Types with a containing non-global namespace also append a leading '.' case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }: - BuildFrom(namespaceSymbol, builder); - builder.Add('.'); - builder.AddRange(symbol.MetadataName.AsSpan()); - break; + { + BuildFrom(namespaceSymbol, target); + target.Add('.'); + target.AddRange(current.MetadataName.AsSpan()); + break; + } - // Nested types append a leading '+' case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }: - BuildFrom(typeSymbol, builder); - builder.Add('+'); - builder.AddRange(symbol.MetadataName.AsSpan()); - break; - default: - break; + { + BuildFrom(typeSymbol, target); + target.Add('+'); + target.AddRange(current.MetadataName.AsSpan()); + break; + } } } diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArrayExtensions.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArrayExtensions.cs new file mode 100644 index 00000000..2ab197f7 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArrayExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Immutable; + +namespace ReactiveUI.SourceGenerators.CodeFixers.Helpers; + +/// Extensions for . +internal static class EquatableArrayExtensions +{ + /// Extension methods for instances. + /// The immutable-array element type. + /// The immutable array to extend. + extension(ImmutableArray array) + where T : IEquatable + { + /// Creates an from the current immutable array. + /// An instance. + internal EquatableArray AsEquatableArray() => new(array); + } +} diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs index d5e8ab07..576089d4 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,102 +7,65 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; namespace ReactiveUI.SourceGenerators.CodeFixers.Helpers; -/// -/// An immutable, equatable array. This is equivalent to but with value equality support. -/// +/// An immutable, equatable array. This is equivalent to but with value equality support. /// The type of values in the array. -/// -/// Creates a new instance. -/// -/// The input to wrap. -internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable +internal readonly struct EquatableArray : IEquatable>, IEnumerable where T : IEquatable { - /// - /// The underlying array. - /// - private readonly T[]? _array = Unsafe.As, T[]?>(ref array); - - /// - /// Gets a value indicating whether the current array is empty. - /// - public bool IsEmpty + /// The underlying array. + private readonly T[]? _array; + + /// Initializes a new instance of the struct. + /// The immutable array to wrap. + internal EquatableArray(ImmutableArray array) => _array = Unsafe.As, T[]?>(ref array); + + /// Gets a value indicating whether the current array is empty. + internal bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => AsImmutableArray().IsEmpty; } - /// - /// Gets a reference to an item at a specified position within the array. - /// + /// Gets a reference to an item at a specified position within the array. /// The index of the item to retrieve a reference to. /// A reference to an item at a specified position within the array. - public ref readonly T this[int index] + internal ref readonly T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref AsImmutableArray().ItemRef(index); } - /// - /// Implicitly converts an to . - /// + /// Implicitly converts an to . /// An instance from a given . public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); - /// - /// Implicitly converts an to . - /// + /// Implicitly converts an to . /// An instance from a given . public static implicit operator ImmutableArray(in EquatableArray array) => array.AsImmutableArray(); - /// - /// Checks whether two values are the same. - /// + /// Checks whether two values are the same. /// The first value. /// The second value. /// Whether and are equal. public static bool operator ==(in EquatableArray left, in EquatableArray right) => left.Equals(right); - /// - /// Checks whether two values are not the same. - /// + /// Checks whether two values are not the same. /// The first value. /// The second value. /// Whether and are not equal. public static bool operator !=(in EquatableArray left, in EquatableArray right) => !left.Equals(right); - /// - /// Creates an instance from a given . - /// - /// The input instance. - /// An instance from a given . - public static EquatableArray FromImmutableArray(ImmutableArray array) => new(array); - - /// - /// Equalses the specified array. - /// - /// The array. - /// A bool. -#pragma warning disable RCS1168 // Parameter name differs from base name - public bool Equals(EquatableArray array) => AsSpan().SequenceEqual(array.AsSpan()); -#pragma warning restore RCS1168 // Parameter name differs from base name - - /// - /// Equalses the specified object. - /// + /// Equalses the specified object. /// The object. /// A bool. public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(array); - /// - /// Gets the hash code. - /// - /// and int. + /// Gets the hash code. + /// A hash code for the current array. public override int GetHashCode() { if (_array is not T[] array) @@ -121,49 +83,36 @@ public override int GetHashCode() return hashCode.ToHashCode(); } - /// - /// Gets an instance from the current . - /// + /// Creates an instance from a given . + /// The input instance. + /// An instance from a given . + internal static EquatableArray FromImmutableArray(ImmutableArray array) => new(array); + + /// Equalses the specified array. + /// The array. + /// A bool. + internal bool Equals(EquatableArray other) => AsSpan().SequenceEqual(other.AsSpan()); + + bool IEquatable>.Equals(EquatableArray other) => Equals(other); + + /// Gets an instance from the current . /// The from the current . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ImmutableArray AsImmutableArray() => Unsafe.As>(ref Unsafe.AsRef(in _array)); + internal ImmutableArray AsImmutableArray() => Unsafe.As>(ref Unsafe.AsRef(in _array)); - /// - /// Returns a wrapping the current items. - /// + /// Returns a wrapping the current items. /// A wrapping the current items. - public ReadOnlySpan AsSpan() => AsImmutableArray().AsSpan(); + internal ReadOnlySpan AsSpan() => AsImmutableArray().AsSpan(); - /// - /// Copies the contents of this instance to a mutable array. - /// + /// Copies the contents of this instance to a mutable array. /// The newly instantiated array. - public T[] ToArray() => [.. AsImmutableArray()]; + internal T[] ToArray() => [.. AsImmutableArray()]; - /// - /// Gets an value to traverse items in the current array. - /// + /// Gets an value to traverse items in the current array. /// An value to traverse items in the current array. - public ImmutableArray.Enumerator GetEnumerator() => AsImmutableArray().GetEnumerator(); + internal ImmutableArray.Enumerator GetEnumerator() => AsImmutableArray().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator(); } - -/// -/// Extensions for . -/// -#pragma warning disable SA1402 // File may only contain a single type -internal static class EquatableArray -#pragma warning restore SA1402 // File may only contain a single type -{ - /// - /// Creates an instance from a given . - /// - /// The type of items in the input array. - /// The input instance. - /// An instance from a given . - public static EquatableArray AsEquatableArray(this ImmutableArray array) - where T : IEquatable => new(array); -} diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs index 3a32d899..bcd8e5a6 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs @@ -1,179 +1,248 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.ComponentModel; +using System; using System.Runtime.CompilerServices; using System.Security.Cryptography; -#pragma warning disable CS0809 +namespace ReactiveUI.SourceGenerators.CodeFixers.Helpers; -namespace System; - -/// -/// A polyfill type that mirrors some methods from on .NET 6. -/// -#pragma warning disable CA1066 // Implement IEquatable when overriding Object.Equals -internal struct HashCode -#pragma warning restore CA1066 // Implement IEquatable when overriding Object.Equals +/// Provides a .NET Standard-compatible incremental hash-code implementation. +internal struct HashCode : IEquatable { - private const uint Prime1 = 2654435761U; - private const uint Prime2 = 2246822519U; - private const uint Prime3 = 3266489917U; - private const uint Prime4 = 668265263U; - private const uint Prime5 = 374761393U; + /// The number of values processed in a complete hash block. + private const uint ValuesPerBlock = 4U; + + /// The queue position following the second entry. + private const uint ThirdQueuePosition = 2U; + + /// The number of bits in a byte. + private const int BitsPerByte = 8; + + /// The first xxHash prime. + private const uint Prime1 = 2_654_435_761U; + + /// The second xxHash prime. + private const uint Prime2 = 2_246_822_519U; + + /// The third xxHash prime. + private const uint Prime3 = 3_266_489_917U; + + /// The fourth xxHash prime. + private const uint Prime4 = 668_265_263U; + + /// The fifth xxHash prime. + private const uint Prime5 = 374_761_393U; + + /// The rotation used by . + private const int RoundRotation = 13; + + /// The rotation used by . + private const int QueueRotation = 17; + + /// The first rotation used while mixing state. + private const int FirstStateRotation = 1; + + /// The second rotation used while mixing state. + private const int SecondStateRotation = 7; + + /// The third rotation used while mixing state. + private const int ThirdStateRotation = 12; + + /// The fourth rotation used while mixing state. + private const int FourthStateRotation = 18; + + /// The number of bytes used to create the random seed. + private const int SeedByteCount = 4; - private static readonly uint seed = GenerateGlobalSeed(); + /// The process-wide random seed. + private static readonly uint Seed = GenerateGlobalSeed(); + /// The first accumulated hash value. private uint _v1; + + /// The second accumulated hash value. private uint _v2; + + /// The third accumulated hash value. private uint _v3; + + /// The fourth accumulated hash value. private uint _v4; + + /// The first queued value. private uint _queue1; + + /// The second queued value. private uint _queue2; + + /// The third queued value. private uint _queue3; + + /// The number of values added to the hash. private uint _length; - /// - /// Adds a single value to the current hash. - /// - /// The type of the value to add into the hash code. - /// The value to add into the hash code. - public void Add(T value) => Add(value?.GetHashCode() ?? 0); - - /// - /// Gets the resulting hashcode from the current instance. - /// - /// The resulting hashcode from the current instance. - public readonly int ToHashCode() - { - var length = _length; - var position = length % 4; - var hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + /// + public override readonly bool Equals(object? obj) => obj is HashCode other && Equals(other); + + /// + public override readonly int GetHashCode() => ToHashCode(); - hash += length * 4; + /// Adds a value to the current hash. + /// The type of the value to add. + /// The value to add. + internal void Add(T value) => Add(value?.GetHashCode() ?? 0); + + /// Gets the resulting hash code from the current instance. + /// The resulting hash code. + internal readonly int ToHashCode() + { + var position = _length % ValuesPerBlock; + var hash = _length < ValuesPerBlock ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + hash += _length * ValuesPerBlock; - if (position > 0) + if (position != 0U) { hash = QueueRound(hash, _queue1); - - if (position > 1) + if (position > 1U) { hash = QueueRound(hash, _queue2); - - if (position > 2) + if (position > ThirdQueuePosition) { hash = QueueRound(hash, _queue3); } } } - hash = MixFinal(hash); - - return (int)hash; + return (int)MixFinal(hash); } -#pragma warning disable CA1065 - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override readonly int GetHashCode() => throw new NotSupportedException(); - - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override readonly bool Equals(object? obj) => throw new NotSupportedException(); -#pragma warning restore CA1065 - - /// - /// Rotates the specified value left by the specified number of bits. - /// Similar in behavior to the x86 instruction ROL. - /// + + /// Determines whether this hash state equals another hash state. + /// The hash state to compare. + /// when both hash states are equal. + internal readonly bool Equals(HashCode other) => + _v1 == other._v1 && _v2 == other._v2 && _v3 == other._v3 && _v4 == other._v4 + && _queue1 == other._queue1 && _queue2 == other._queue2 && _queue3 == other._queue3 + && _length == other._length; + + /// Rotates a value left by a number of bits. /// The value to rotate. - /// The number of bits to rotate by. - /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The number of bits to rotate by. /// The rotated value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> (32 - offset)); + private static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> ((sizeof(uint) * BitsPerByte) - offset)); - /// - /// Initializes the default seed. - /// + /// Creates the process-wide random seed. /// A random seed. - private static unsafe uint GenerateGlobalSeed() + private static uint GenerateGlobalSeed() { - var bytes = new byte[4]; - - using (var generator = RandomNumberGenerator.Create()) - { - generator.GetBytes(bytes); - } - + var bytes = new byte[SeedByteCount]; + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(bytes); return BitConverter.ToUInt32(bytes, 0); } + /// Initializes a full hash block. + /// The first initialized value. + /// The second initialized value. + /// The third initialized value. + /// The fourth initialized value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) { - v1 = seed + Prime1 + Prime2; - v2 = seed + Prime2; - v3 = seed; - v4 = seed - Prime1; + v1 = Seed + Prime1 + Prime2; + v2 = Seed + Prime2; + v3 = Seed; + v4 = Seed - Prime1; } + /// Mixes a value into a hash accumulator. + /// The current hash. + /// The value to mix. + /// The mixed hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint Round(uint hash, uint input) => RotateLeft(hash + (input * Prime2), 13) * Prime1; + private static uint Round(uint hash, uint input) => RotateLeft(hash + (input * Prime2), RoundRotation) * Prime1; + /// Mixes a queued value into a hash accumulator. + /// The current hash. + /// The queued value to mix. + /// The mixed hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint QueueRound(uint hash, uint queuedValue) => RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; - + private static uint QueueRound(uint hash, uint queuedValue) => RotateLeft(hash + (queuedValue * Prime3), QueueRotation) * Prime4; + + /// Mixes a complete hash state. + /// The first state value. + /// The second state value. + /// The third state value. + /// The fourth state value. + /// The mixed hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixState(uint v1, uint v2, uint v3, uint v4) => RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + private static uint MixState(uint v1, uint v2, uint v3, uint v4) => + RotateLeft(v1, FirstStateRotation) + RotateLeft(v2, SecondStateRotation) + + RotateLeft(v3, ThirdStateRotation) + RotateLeft(v4, FourthStateRotation); + /// Creates the initial hash state for an empty sequence. + /// The initial hash state. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixEmptyState() => seed + Prime5; + private static uint MixEmptyState() => Seed + Prime5; + /// Applies the final avalanche to a hash. + /// The hash to finalize. + /// The finalized hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint MixFinal(uint hash) { hash ^= hash >> 15; hash *= Prime2; - hash ^= hash >> 13; + hash ^= hash >> RoundRotation; hash *= Prime3; hash ^= hash >> 16; - return hash; } + /// Adds an integer value to the current hash. + /// The value to add. private void Add(int value) { - var val = (uint)value; - var previousLength = _length++; - var position = previousLength % 4; - - if (position == 0) - { - _queue1 = val; - } - else if (position == 1) - { - _queue2 = val; - } - else if (position == 2) - { - _queue3 = val; - } - else + var previousLength = _length; + _length++; + switch (previousLength % ValuesPerBlock) { - if (previousLength == 3) - { - Initialize(out _v1, out _v2, out _v3, out _v4); - } + case 0U: + { + _queue1 = (uint)value; + return; + } + + case 1U: + { + _queue2 = (uint)value; + return; + } + + case ThirdQueuePosition: + { + _queue3 = (uint)value; + return; + } - _v1 = Round(_v1, _queue1); - _v2 = Round(_v2, _queue2); - _v3 = Round(_v3, _queue3); - _v4 = Round(_v4, val); + default: + { + if (previousLength == ValuesPerBlock - 1U) + { + Initialize(out _v1, out _v2, out _v3, out _v4); + } + + _v1 = Round(_v1, _queue1); + _v2 = Round(_v2, _queue2); + _v3 = Round(_v3, _queue3); + _v4 = Round(_v4, (uint)value); + return; + } } } + + /// + bool IEquatable.Equals(HashCode other) => Equals(other); } diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs index 09dfeea8..9579eb4f 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -13,85 +12,72 @@ namespace ReactiveUI.SourceGenerators.CodeFixers.Helpers; -/// -/// A helper type to build sequences of values with pooled buffers. -/// +/// A helper type to build sequences of values with pooled buffers. /// The type of items to create sequences for. internal ref struct ImmutableArrayBuilder { - /// - /// The rented instance to use. - /// + /// The rented instance to use. private Writer? _writer; - /// - /// Initializes a new instance of the struct. - /// + /// Initializes a new instance of the struct. /// The target data writer to use. private ImmutableArrayBuilder(Writer writer) => _writer = writer; - /// - /// Gets the data written to the underlying buffer so far, as a . - /// + /// Gets the data written to the underlying buffer so far, as a . [UnscopedRef] - public readonly ReadOnlySpan WrittenSpan + internal readonly ReadOnlySpan WrittenSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _writer!.WrittenSpan; } - /// - /// Gets the count. - /// + /// Gets the count. /// /// The count. /// - public readonly int Count + internal readonly int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _writer!.Count; } - /// - /// Creates a value with a pooled underlying data writer. - /// + /// + public override readonly string ToString() => _writer!.WrittenSpan.ToString(); + + /// Creates a value with a pooled underlying data writer. /// A instance to write data to. - public static ImmutableArrayBuilder Rent() => new(new Writer()); + internal static ImmutableArrayBuilder Rent() => new(new Writer()); - /// - public readonly void Add(T item) => _writer!.Add(item); + /// Adds an item to the end of the builder. + /// The item to add. + internal readonly void Add(T item) => _writer!.Add(item); - /// - /// Adds the specified items to the end of the array. - /// + /// Adds the specified items to the end of the array. /// The items to add at the end of the array. - public readonly void AddRange(scoped in ReadOnlySpan items) => _writer!.AddRange(items); + internal readonly void AddRange(scoped in ReadOnlySpan items) => _writer!.AddRange(items); - /// - public readonly ImmutableArray ToImmutable() + /// Creates an immutable array from the values in the builder. + /// An immutable array containing the builder's values. + internal readonly ImmutableArray ToImmutable() { var array = _writer!.WrittenSpan.ToArray(); return Unsafe.As>(ref array); } - /// - public readonly T[] ToArray() => _writer!.WrittenSpan.ToArray(); + /// Creates an array from the values in the builder. + /// An array containing the builder's values. + internal readonly T[] ToArray() => _writer!.WrittenSpan.ToArray(); - /// - /// Gets an instance for the current builder. - /// + /// Gets an instance for the current builder. /// An instance for the current builder. /// /// The builder should not be mutated while an enumerator is in use. /// - public readonly IEnumerable AsEnumerable() => _writer!; - - /// - public override readonly string ToString() => _writer!.WrittenSpan.ToString(); + internal readonly IEnumerable AsEnumerable() => _writer!; - /// - public void Dispose() + /// Returns the pooled buffer to its owner. + internal void Dispose() { var writer = _writer; @@ -100,53 +86,55 @@ public void Dispose() writer?.Dispose(); } - /// - /// A class handling the actual buffer writing. - /// + /// A class handling the actual buffer writing. private sealed class Writer : ICollection, IDisposable { - /// - /// The underlying array. - /// + /// The initial buffer capacity for character builders. + private const int CharacterBufferCapacity = 1_024; + + /// The initial buffer capacity for non-character builders. + private const int DefaultBufferCapacity = 8; + + /// The underlying array. private T?[]? _array; - /// - /// Initializes a new instance of the class. - /// Creates a new instance with the specified parameters. - /// - public Writer() + /// Initializes a new instance of the class. Creates a new instance with the specified parameters. + internal Writer() { - _array = ArrayPool.Shared.Rent(typeof(T) == typeof(char) ? 1024 : 8); + _array = ArrayPool.Shared.Rent(typeof(T) == typeof(char) ? CharacterBufferCapacity : DefaultBufferCapacity); Count = 0; } - /// - public int Count + /// Gets or sets gets the number of values in the buffer. + internal int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get; private set; + get; set; } - /// - public ReadOnlySpan WrittenSpan + /// Gets a span over the values in the buffer. + internal ReadOnlySpan WrittenSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_array!, 0, Count); } - /// + /// Gets a value indicating whether this collection is read-only. bool ICollection.IsReadOnly => true; - /// - public void Add(T item) + /// Adds an item to the buffer. + /// The item to add. + internal void Add(T item) { EnsureCapacity(1); - _array![Count++] = item; + _array![Count] = item; + Count++; } - /// - public void AddRange(in ReadOnlySpan items) + /// Adds a range of items to the buffer. + /// The items to add. + internal void AddRange(in ReadOnlySpan items) { EnsureCapacity(items.Length); @@ -155,26 +143,32 @@ public void AddRange(in ReadOnlySpan items) Count += items.Length; } - /// - public void Dispose() + /// Returns the pooled buffer. + internal void Dispose() { var array = _array; _array = null; - if (array is not null) + if (array is null) { - ArrayPool.Shared.Return(array, clearArray: typeof(T) != typeof(char)); + return; } + + ArrayPool.Shared.Return(array, clearArray: typeof(T) != typeof(char)); } - /// + /// Clears this collection. void ICollection.Clear() => throw new NotSupportedException(); - /// + /// Determines whether this collection contains an item. + /// The item to locate. + /// when the item is in the collection. bool ICollection.Contains(T item) => throw new NotSupportedException(); - /// + /// Copies the collection to an array. + /// The destination array. + /// The destination index at which copying begins. void ICollection.CopyTo(T[] array, int arrayIndex) => Array.Copy(_array!, 0, array, arrayIndex, Count); /// @@ -189,28 +183,39 @@ IEnumerator IEnumerable.GetEnumerator() } } - /// + /// Gets a non-generic enumerator for this collection. + /// A non-generic enumerator for this collection. IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); - /// + /// Removes an item from this collection. + /// The item to remove. + /// when the item was removed. bool ICollection.Remove(T item) => throw new NotSupportedException(); - /// - /// Ensures that has enough free space to contain a given number of new items. - /// + /// Gets the number of items in this collection. + int ICollection.Count => Count; + + /// Adds an item through the collection interface. + /// The item to add. + void ICollection.Add(T item) => Add(item); + + /// Disposes this collection through the disposable interface. + void IDisposable.Dispose() => Dispose(); + + /// Ensures that has enough free space to contain a given number of new items. /// The minimum number of items to ensure space for in . [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureCapacity(int requestedSize) { - if (requestedSize > _array!.Length - Count) + if (requestedSize <= _array!.Length - Count) { - ResizeBuffer(requestedSize); + return; } + + ResizeBuffer(requestedSize); } - /// - /// Resizes to ensure it can fit the specified number of new items. - /// + /// Resizes to ensure it can fit the specified number of new items. /// The minimum number of items to ensure space for in . [MethodImpl(MethodImplOptions.NoInlining)] private void ResizeBuffer(int sizeHint) diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs index a2a0ad5c..dce1d08a 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs @@ -1,11 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -15,103 +14,208 @@ namespace ReactiveUI.SourceGenerators.CodeFixers; -/// -/// PropertyToFieldAnalyzer. -/// +/// Reports properties that can be converted to ReactiveUI reactive fields. [DiagnosticAnalyzer(LanguageNames.CSharp)] -public class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer +public sealed class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer { - /// - /// Gets the supported diagnostics. - /// - /// - /// The supported diagnostics. - /// + /// Gets the supported diagnostics. public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(PropertyToReactiveFieldRule); - /// - /// Initializes the specified context. - /// - /// The context. + /// Gets the type names excluded from property conversion. + private static IReadOnlyList IgnoredTypeNames { get; } = ["ReactiveCommand", "ReactiveProperty", "ViewModelActivator"]; + + /// Initializes this analyzer. + /// The analysis context. public override void Initialize(AnalysisContext context) { if (context is null) { - throw new System.ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(context)); } context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration); + context.RegisterSyntaxNodeAction(static nodeContext => AnalyzeNode(in nodeContext), SyntaxKind.PropertyDeclaration); } - private void AnalyzeNode(SyntaxNodeAnalysisContext context) + /// Analyzes a property declaration. + /// The syntax-node analysis context. + private static void AnalyzeNode(in SyntaxNodeAnalysisContext context) { - var symbol = context.ContainingSymbol; - if (symbol is not IPropertySymbol propertySymbol) + if (context.ContainingSymbol is not IPropertySymbol propertySymbol + || context.Node is not PropertyDeclarationSyntax propertyDeclaration + || !IsCandidate(propertySymbol, propertyDeclaration)) { return; } - if (context.Node is not PropertyDeclarationSyntax propertyDeclaration) + context.ReportDiagnostic(Diagnostic.Create(PropertyToReactiveFieldRule, propertyDeclaration.GetLocation())); + } + + /// Determines whether a property declaration is suitable for conversion. + /// The property symbol. + /// The property declaration syntax. + /// when the property can be converted. + private static bool IsCandidate(IPropertySymbol propertySymbol, PropertyDeclarationSyntax propertyDeclaration) => + IsReactiveType(propertySymbol, propertyDeclaration) + && propertySymbol.SetMethod is not null + && !HasReactiveAttribute(propertySymbol, propertyDeclaration) + && IsPublicInstanceAutoProperty(propertyDeclaration) + && !HasRestrictedSetter(propertyDeclaration) + && !HasIgnoredTypeName(propertyDeclaration); + + /// Determines whether a property belongs to a ReactiveUI-compatible type. + /// The property symbol. + /// The property declaration syntax. + /// when the containing type is ReactiveUI-compatible. + private static bool IsReactiveType(IPropertySymbol propertySymbol, PropertyDeclarationSyntax propertyDeclaration) => + propertySymbol.IsTargetTypeValid() || HasReactiveBaseOrInterface(propertyDeclaration); + + /// Determines whether a type declaration explicitly names a ReactiveUI base type or interface. + /// The property declaration. + /// when a ReactiveUI base type or interface is declared. + private static bool HasReactiveBaseOrInterface(PropertyDeclarationSyntax propertyDeclaration) + { + var baseTypes = propertyDeclaration.FirstAncestorOrSelf()?.BaseList?.Types; + if (baseTypes is null) { - return; + return false; + } + + foreach (var baseType in baseTypes) + { + if (IsReactiveTypeName(baseType.Type)) + { + return true; + } } - // Make sure the property is part of a class inherited from ReactiveObject. - // In some test harnesses, ReactiveUI types might not be referenced, so the semantic - // check can fail even when the syntax clearly indicates intended usage. - var isTargetTypeValid = propertySymbol.IsTargetTypeValid(); + return false; + } - if (!isTargetTypeValid) + /// Determines whether a type syntax names a ReactiveUI base type or interface. + /// The type syntax to inspect. + /// when the type syntax has a recognized ReactiveUI name. + private static bool IsReactiveTypeName(TypeSyntax typeSyntax) + { + var typeName = typeSyntax switch { - var containingTypeSyntax = propertyDeclaration.FirstAncestorOrSelf(); + IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, + QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText, + _ => null + }; - var baseTypes = containingTypeSyntax?.BaseList?.Types; - var hasReactiveBaseOrInterfaceInBaseList = baseTypes?.Any(t => - t.Type is IdentifierNameSyntax { Identifier.ValueText: "ReactiveObject" } || - t.Type is QualifiedNameSyntax { Right.Identifier.ValueText: "ReactiveObject" } || - t.Type is IdentifierNameSyntax { Identifier.ValueText: "IReactiveObject" } || - t.Type is QualifiedNameSyntax { Right.Identifier.ValueText: "IReactiveObject" }) == true; + return typeName is "ReactiveObject" or "IReactiveObject"; + } - if (!hasReactiveBaseOrInterfaceInBaseList) + /// Determines whether a property has a ReactiveUI attribute. + /// The property symbol. + /// The property declaration syntax. + /// when the property has a ReactiveUI attribute. + private static bool HasReactiveAttribute(IPropertySymbol propertySymbol, PropertyDeclarationSyntax propertyDeclaration) => + HasSemanticReactiveAttribute(propertySymbol) || HasReactiveSyntaxAttribute(propertyDeclaration); + + /// Determines whether a property symbol has a ReactiveUI attribute. + /// The property symbol. + /// when the property has a ReactiveUI attribute. + private static bool HasSemanticReactiveAttribute(IPropertySymbol propertySymbol) + { + foreach (var attribute in propertySymbol.GetAttributes()) + { + if (attribute.AttributeClass?.Name is "ReactiveAttribute" or "ObservableAsProperty") { - return; + return true; } } - // Check if the property is an readonly property - if (propertySymbol.SetMethod == null) + return false; + } + + /// Determines whether property syntax has a ReactiveUI attribute. + /// The property declaration syntax. + /// when the property syntax has a ReactiveUI attribute. + private static bool HasReactiveSyntaxAttribute(PropertyDeclarationSyntax propertyDeclaration) + { + foreach (var attributeList in propertyDeclaration.AttributeLists) { - return; + foreach (var attribute in attributeList.Attributes) + { + if (attribute.Name is IdentifierNameSyntax { Identifier.ValueText: "Reactive" } + or QualifiedNameSyntax { Right.Identifier.ValueText: "Reactive" }) + { + return true; + } + } } - // Check if the property is a ReactiveUI property - if (propertySymbol.GetAttributes().Any(a => a.AttributeClass?.Name == "ReactiveAttribute" || a.AttributeClass?.Name == "ObservableAsProperty")) + return false; + } + + /// Determines whether a declaration is a public instance auto-property. + /// The property declaration syntax. + /// when the declaration is a public instance auto-property. + private static bool IsPublicInstanceAutoProperty(PropertyDeclarationSyntax propertyDeclaration) + { + if (propertyDeclaration.ExpressionBody is not null + || !propertyDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword) + || propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)) { - return; + return false; } - // If semantic attributes are not available, fall back to syntax to detect [Reactive]. - if (propertyDeclaration.AttributeLists - .SelectMany(static a => a.Attributes) - .Any(a => a.Name is IdentifierNameSyntax { Identifier.ValueText: "Reactive" } || - a.Name is QualifiedNameSyntax { Right.Identifier.ValueText: "Reactive" })) + if (propertyDeclaration.AccessorList is null) { - return; + return true; + } + + foreach (var accessor in propertyDeclaration.AccessorList.Accessors) + { + if (accessor.Body is not null || accessor.ExpressionBody is not null) + { + return false; + } } - var isAutoProperty = propertyDeclaration.ExpressionBody == null && (propertyDeclaration.AccessorList?.Accessors.All(a => a.Body == null && a.ExpressionBody == null) != false); - var hasCorrectModifiers = propertyDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword) && !propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword); - var doesNotHavePrivateSetOrInternalSet = propertyDeclaration.AccessorList?.Accessors.Any(a => a.Modifiers.Any(SyntaxKind.PrivateKeyword) || a.Modifiers.Any(SyntaxKind.InternalKeyword)) == false; - var namesToIgnore = new List { "ReactiveCommand", "ReactiveProperty", "ViewModelActivator" }; - var isNotIgnored = !namesToIgnore.Any(n => propertyDeclaration.Type.ToString().Contains(n)); + return true; + } - if (isAutoProperty && hasCorrectModifiers && doesNotHavePrivateSetOrInternalSet && isNotIgnored) + /// Determines whether an accessor has a private or internal setter. + /// The property declaration syntax. + /// when a setter has restricted visibility. + private static bool HasRestrictedSetter(PropertyDeclarationSyntax propertyDeclaration) + { + if (propertyDeclaration.AccessorList is null) { - var diagnostic = Diagnostic.Create(PropertyToReactiveFieldRule, propertyDeclaration.GetLocation()); - context.ReportDiagnostic(diagnostic); + return false; } + + foreach (var accessor in propertyDeclaration.AccessorList.Accessors) + { + if (accessor.Modifiers.Any(SyntaxKind.PrivateKeyword) + || accessor.Modifiers.Any(SyntaxKind.InternalKeyword)) + { + return true; + } + } + + return false; + } + + /// Determines whether a property has a type name excluded from conversion. + /// The property declaration syntax. + /// when the property type is excluded from conversion. + private static bool HasIgnoredTypeName(PropertyDeclarationSyntax propertyDeclaration) + { + var typeName = propertyDeclaration.Type.ToString(); + foreach (var ignoredName in IgnoredTypeNames) + { + if (typeName.Contains(ignoredName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; } } diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldCodeFixProvider.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldCodeFixProvider.cs index b5419367..b37f34b8 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldCodeFixProvider.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldCodeFixProvider.cs @@ -1,87 +1,86 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Collections.Generic; using System.Collections.Immutable; -using System.Composition; -using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static ReactiveUI.SourceGenerators.CodeFixers.Diagnostics.DiagnosticDescriptors; namespace ReactiveUI.SourceGenerators.CodeFixers; -/// -/// PropertyToFieldCodeFixProvider. -/// -/// +/// Provides fixes that convert eligible properties to reactive fields. [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PropertyToReactiveFieldCodeFixProvider))] -[Shared] -public class PropertyToReactiveFieldCodeFixProvider : CodeFixProvider +public sealed class PropertyToReactiveFieldCodeFixProvider : CodeFixProvider { - /// - /// Gets a list of diagnostic IDs that this provider can provide fixes for. - /// - public sealed override ImmutableArray FixableDiagnosticIds => + /// Gets a list of diagnostic IDs that this provider can fix. + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(PropertyToReactiveFieldRule.Id); - /// - /// Gets an optional that can fix all/multiple occurrences of diagnostics fixed by this code fix provider. - /// Return null if the provider doesn't support fix all/multiple occurrences. - /// Otherwise, you can return any of the well known fix all providers from or implement your own fix all provider. - /// - /// FixAllProvider. - public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// Gets the batch fix provider. + /// The batch fix provider. + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - /// - /// Computes one or more fixes for the specified . - /// - /// A containing context information about the diagnostics to fix. - /// The context must only contain diagnostics with a included in the for the current provider. - /// A representing the asynchronous operation. - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + /// Registers the available fixes. + /// The code-fix context. + /// A task that completes after the fixes are registered. + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var diagnostic = context.Diagnostics[0]; - var diagnosticSpan = diagnostic.Location.SourceSpan; - - // Find the property declaration syntax node - var propertyDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + var propertyDeclaration = FindPropertyDeclaration(root, diagnostic.Location.SourceSpan.Start); + if (propertyDeclaration is null) + { + return; + } - var fieldName = propertyDeclaration?.Identifier.Text; - fieldName = "_" + fieldName?.Substring(0, 1).ToLower() + fieldName?.Substring(1); + var propertyName = propertyDeclaration.Identifier.Text; + var fieldName = $"_{char.ToLowerInvariant(propertyName[0])}{propertyName.Remove(0, 1)}"; + var attributeSyntaxes = new List(); + foreach (var attributeList in propertyDeclaration.AttributeLists) + { + attributeSyntaxes.Add(AttributeList(attributeList.Attributes)); + } - var attributeSyntaxes = - propertyDeclaration!.AttributeLists - .Select(static a => AttributeList(a.Attributes)).ToList(); attributeSyntaxes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ReactiveUI.SourceGenerators.Reactive"))))); - SyntaxList al = new(attributeSyntaxes); - - // Create a new field declaration syntax node var fieldDeclaration = FieldDeclaration( - VariableDeclaration(propertyDeclaration!.Type) + VariableDeclaration(propertyDeclaration.Type) .WithVariables(SingletonSeparatedList( VariableDeclarator(fieldName).WithInitializer(propertyDeclaration.Initializer)))) - .WithAttributeLists(al) + .WithAttributeLists(new(attributeSyntaxes)) .WithLeadingTrivia(propertyDeclaration.GetLeadingTrivia()) .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword))); - - // Replace the property with the field var newRoot = root?.ReplaceNode(propertyDeclaration, fieldDeclaration); - // Apply the code fix context.RegisterCodeFix( CodeAction.Create( "Convert to Reactive field", - c => Task.FromResult(context.Document.WithSyntaxRoot(newRoot!)), + _ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot!)), "Convert to Reactive field"), diagnostic); } + + /// Finds the property declaration containing a source position. + /// The syntax-tree root. + /// The source position. + /// The containing property declaration, when present. + private static PropertyDeclarationSyntax? FindPropertyDeclaration(SyntaxNode? root, int position) + { + for (SyntaxNode? current = root?.FindToken(position).Parent; current is not null; current = current.Parent) + { + if (current is PropertyDeclarationSyntax propertyDeclaration) + { + return propertyDeclaration; + } + } + + return null; + } } diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseAnalyzer.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseAnalyzer.cs index 4e978022..df85cd3e 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseAnalyzer.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseAnalyzer.cs @@ -1,11 +1,9 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,22 +12,16 @@ namespace ReactiveUI.SourceGenerators.CodeFixers; -/// -/// ReactiveAttributeMisuseAnalyzer. -/// +/// Reports ReactiveUI properties whose partial declarations are incomplete. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class ReactiveAttributeMisuseAnalyzer : DiagnosticAnalyzer { - /// - /// Gets a set of descriptors for the diagnostics that this analyzer is capable of producing. - /// + /// Gets a set of descriptors for the diagnostics that this analyzer is capable of producing. public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(ReactiveAttributeRequiresPartialRule); - /// - /// Called once at session start to register actions in the analysis context. - /// + /// Called once at session start to register actions in the analysis context. /// The analysis context. /// context. public override void Initialize(AnalysisContext context) @@ -41,10 +33,12 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeProperty, SyntaxKind.PropertyDeclaration); + context.RegisterSyntaxNodeAction(static nodeContext => AnalyzeProperty(in nodeContext), SyntaxKind.PropertyDeclaration); } - private static void AnalyzeProperty(SyntaxNodeAnalysisContext context) + /// Analyzes a property declaration for a ReactiveUI attribute. + /// The syntax-node analysis context. + private static void AnalyzeProperty(in SyntaxNodeAnalysisContext context) { if (context.Node is not PropertyDeclarationSyntax property) { @@ -71,6 +65,9 @@ private static void AnalyzeProperty(SyntaxNodeAnalysisContext context) context.ReportDiagnostic(Diagnostic.Create(ReactiveAttributeRequiresPartialRule, property.Identifier.GetLocation())); } + /// Determines whether an attribute list contains the ReactiveUI reactive attribute. + /// The attribute lists to inspect. + /// when the ReactiveUI reactive attribute is present. private static bool HasReactiveAttribute(SyntaxList attributeLists) { foreach (var list in attributeLists) diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseCodeFixProvider.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseCodeFixProvider.cs index 240d5712..778223da 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseCodeFixProvider.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/ReactiveAttributeMisuseCodeFixProvider.cs @@ -1,52 +1,36 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Immutable; -using System.Composition; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Editing; using static ReactiveUI.SourceGenerators.CodeFixers.Diagnostics.DiagnosticDescriptors; namespace ReactiveUI.SourceGenerators.CodeFixers; -/// -/// ReactiveAttributeMisuseCodeFixProvider. -/// -/// +/// Provides fixes for incomplete partial declarations using Reactive. [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ReactiveAttributeMisuseCodeFixProvider))] -[Shared] public sealed class ReactiveAttributeMisuseCodeFixProvider : CodeFixProvider { - /// - /// Gets a list of diagnostic IDs that this provider can provide fixes for. - /// + /// Gets a list of diagnostic IDs that this provider can fix. public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(ReactiveAttributeRequiresPartialRule.Id); - /// - /// Gets an optional that can fix all/multiple occurrences of diagnostics fixed by this code fix provider. - /// Return null if the provider doesn't support fix all/multiple occurrences. - /// Otherwise, you can return any of the well known fix all providers from or implement your own fix all provider. - /// - /// A instance if the provider supports fix all/multiple occurrences; otherwise, null. + /// Gets the batch fix provider. + /// The batch fix provider. public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - /// - /// Computes one or more fixes for the specified . - /// - /// A containing context information about the diagnostics to fix. - /// The context must only contain diagnostics with a included in the for the current provider. - /// A representing the asynchronous operation. + /// Registers the available fixes. + /// The code-fix context. + /// A task that completes after the fixes are registered. public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); @@ -55,15 +39,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) return; } - var diagnostic = context.Diagnostics.First(); - var diagnosticSpan = diagnostic.Location.SourceSpan; - - var propertyDeclaration = root.FindToken(diagnosticSpan.Start) - .Parent? - .AncestorsAndSelf() - .OfType() - .FirstOrDefault(); - + var diagnostic = context.Diagnostics[0]; + var propertyDeclaration = FindPropertyDeclaration(root, diagnostic.Location.SourceSpan.Start); if (propertyDeclaration is null) { return; @@ -72,71 +49,107 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( title: "Make property and containing type partial", - createChangedDocument: c => MakePartialAsync(context.Document, propertyDeclaration, c), + createChangedDocument: cancellationToken => MakePartialAsync(context.Document, propertyDeclaration, cancellationToken), equivalenceKey: "Make property and containing type partial"), diagnostic); } + /// Makes a property and its containing type partial. + /// The source document. + /// The property to update. + /// The cancellation token. + /// The updated document. private static async Task MakePartialAsync(Document document, PropertyDeclarationSyntax property, CancellationToken cancellationToken) { var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - - var newProperty = AddPartialModifier(property); - editor.ReplaceNode(property, newProperty); + editor.ReplaceNode(property, AddPartialModifier(property)); var containingType = property.FirstAncestorOrSelf(); if (containingType is not null) { - var newContainingType = AddPartialModifier(containingType); - editor.ReplaceNode(containingType, newContainingType); + editor.ReplaceNode(containingType, AddPartialModifier(containingType)); } return editor.GetChangedDocument(); } + /// Adds a partial modifier when a declaration does not already have one. + /// The declaration type. + /// The declaration to update. + /// The declaration with a partial modifier. private static T AddPartialModifier(T declaration) where T : MemberDeclarationSyntax { var modifiers = declaration switch { - TypeDeclarationSyntax t => t.Modifiers, - PropertyDeclarationSyntax p => p.Modifiers, + TypeDeclarationSyntax typeDeclaration => typeDeclaration.Modifiers, + PropertyDeclarationSyntax propertyDeclaration => propertyDeclaration.Modifiers, _ => throw new InvalidOperationException("Unsupported declaration type") }; - if (modifiers.Any(SyntaxKind.PartialKeyword)) + if (HasModifier(in modifiers, SyntaxKind.PartialKeyword)) { return declaration; } - var partialToken = SyntaxFactory.Token(SyntaxKind.PartialKeyword); - - // Insert `partial` after the last accessibility modifier (if present). var insertIndex = 0; for (var i = 0; i < modifiers.Count; i++) { - if (modifiers[i].IsKind(SyntaxKind.PublicKeyword) - || modifiers[i].IsKind(SyntaxKind.InternalKeyword) - || modifiers[i].IsKind(SyntaxKind.PrivateKeyword) - || modifiers[i].IsKind(SyntaxKind.ProtectedKeyword)) + if (IsAccessibilityModifier(modifiers[i]) || modifiers[i].IsKind(SyntaxKind.RequiredKeyword)) { insertIndex = i + 1; } + } + + var newModifiers = modifiers.Insert(insertIndex, SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + return declaration switch + { + TypeDeclarationSyntax typeDeclaration => (T)(MemberDeclarationSyntax)typeDeclaration.WithModifiers(newModifiers), + PropertyDeclarationSyntax propertyDeclaration => (T)(MemberDeclarationSyntax)propertyDeclaration.WithModifiers(newModifiers), + _ => declaration + }; + } - // `required` must precede `partial` for required members. - if (modifiers[i].IsKind(SyntaxKind.RequiredKeyword)) + /// Finds the property declaration containing a source position. + /// The syntax-tree root. + /// The source position. + /// The containing property declaration, when present. + private static PropertyDeclarationSyntax? FindPropertyDeclaration(SyntaxNode root, int position) + { + for (SyntaxNode? current = root.FindToken(position).Parent; current is not null; current = current.Parent) + { + if (current is PropertyDeclarationSyntax propertyDeclaration) { - insertIndex = i + 1; + return propertyDeclaration; } } - var newModifiers = modifiers.Insert(insertIndex, partialToken); + return null; + } - return declaration switch + /// Determines whether a modifier list contains a token of the specified kind. + /// The modifiers to inspect. + /// The token kind to find. + /// when the token is present. + private static bool HasModifier(in SyntaxTokenList modifiers, SyntaxKind kind) + { + foreach (var modifier in modifiers) { - TypeDeclarationSyntax t => (T)(MemberDeclarationSyntax)t.WithModifiers(newModifiers), - PropertyDeclarationSyntax p => (T)(MemberDeclarationSyntax)p.WithModifiers(newModifiers), - _ => declaration - }; + if (modifier.IsKind(kind)) + { + return true; + } + } + + return false; } + + /// Determines whether a modifier is an accessibility modifier. + /// The modifier to inspect. + /// when the modifier controls accessibility. + private static bool IsAccessibilityModifier(SyntaxToken modifier) => + modifier.IsKind(SyntaxKind.PublicKeyword) + || modifier.IsKind(SyntaxKind.InternalKeyword) + || modifier.IsKind(SyntaxKind.PrivateKeyword) + || modifier.IsKind(SyntaxKind.ProtectedKeyword); } diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/DiagnosticDescriptors.cs index 90d00eb9..f0a19d26 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/DiagnosticDescriptors.cs @@ -1,23 +1,16 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; -#pragma warning disable IDE0090 // Use 'new DiagnosticDescriptor(...)' - namespace ReactiveUI.SourceGenerators.CodeFixers.Diagnostics; -/// -/// A container for all instances for errors reported by analyzers in this project. -/// +/// A container for all instances for errors reported by analyzers in this project. internal static class DiagnosticDescriptors { - /// - /// The property to field rule. - /// - public static readonly DiagnosticDescriptor PropertyToReactiveFieldRule = new( + /// The property to field rule. + internal static readonly DiagnosticDescriptor PropertyToReactiveFieldRule = new( id: "RXUISG0016", title: "Property To Reactive Field, change to `[Reactive]` private type _fieldName;", messageFormat: "Replace the property with a INPC Reactive Property for ReactiveUI", @@ -27,10 +20,8 @@ internal static class DiagnosticDescriptors description: "Used to create a Read Write INPC Reactive Property for ReactiveUI, annotated with `[Reactive]`.", helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); - /// - /// The `[Reactive]` attribute was used on a property, but required `partial` modifiers are missing. - /// - public static readonly DiagnosticDescriptor ReactiveAttributeRequiresPartialRule = new( + /// The `[Reactive]` attribute was used on a property, but required `partial` modifiers are missing. + internal static readonly DiagnosticDescriptor ReactiveAttributeRequiresPartialRule = new( id: "RXUISG0020", title: "[Reactive] requires partial property and containing type", messageFormat: "`[Reactive]` requires the property to be `partial` and the containing type to be partial so source generation can run", diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/ReactiveUI.SourceGenerators.Analyzers.CodeFixes.csproj b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/ReactiveUI.SourceGenerators.Analyzers.CodeFixes.csproj index 697cd11a..3b4f7c10 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/ReactiveUI.SourceGenerators.Analyzers.CodeFixes.csproj +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/ReactiveUI.SourceGenerators.Analyzers.CodeFixes.csproj @@ -1,5 +1,4 @@ - - + $(RoslynTfm) enable @@ -12,11 +11,9 @@ true true true - $(NoWarn);RS1038;RS2007;NU5128 A MVVM framework that integrates with the Reactive Extensions for .NET to create elegant, testable User Interfaces that run on any mobile or desktop platform. This is the Source Generators package for ReactiveUI - diff --git a/src/ReactiveUI.SourceGenerators.Execute.Maui/IViewForTest.cs b/src/ReactiveUI.SourceGenerators.Execute.Maui/IViewForTest.cs index 98055ece..523fd796 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Maui/IViewForTest.cs +++ b/src/ReactiveUI.SourceGenerators.Execute.Maui/IViewForTest.cs @@ -1,17 +1,12 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Diagnostics.CodeAnalysis; using ReactiveUI.SourceGenerators; -namespace SGReactiveUI.SourceGenerators.Test.Maui -{ - /// - /// IViewForTest. - /// - /// - [IViewFor] - public partial class IViewForTest : Shell; -} +namespace SGReactiveUI.SourceGenerators.Test.Maui; + +/// Provides the MAUI test view used to exercise generated IViewFor members. +/// +[IViewFor] +public partial class IViewForTest : Shell; diff --git a/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj b/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj index a07581e8..d5950308 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj @@ -1,4 +1,4 @@ - + $(MauiTfms) @@ -18,7 +18,7 @@ - + @@ -28,4 +28,8 @@ + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute.Maui/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute.Maui/TestViewModel.cs index 0f89cc33..893af8f7 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Maui/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute.Maui/TestViewModel.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; @@ -8,144 +7,186 @@ using System.Reactive.Linq; using System.Runtime.Serialization; using System.Text.Json.Serialization; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestClass. -/// +/// Provides a MAUI sample that exercises generated reactive members and commands. [ExcludeFromCodeCoverage] [DataContract] public partial class TestViewModel : ReactiveObject { + /// Represents the argument supplied to the observable command. + private const int ObservableCommandArgument = 100; + + /// Represents the argument supplied to the point-producing command. + private const int PointCommandArgument = 200; + + /// Represents the coordinate used by the point-producing sample command. + private const int PointCoordinate = 100; + + /// Represents the offset applied by the observable sample command. + private const double ObservableCommandOffset = 10.0D; + + /// Represents the delay before the cancellation subscription is disposed. + private const int CancellationObservationDelayMilliseconds = 1_000; + + /// Represents the duration of the cancellable sample command. + private const int CancellableCommandDelayMilliseconds = 2_000; + + /// Stores the observable-as-property sample value. [JsonInclude] [DataMember] - [ObservableAsProperty] + [ObservableAsProperty(ReadOnly = false)] private double _test2Property; + /// Stores the reactive property sample value. [JsonInclude] [Reactive] [DataMember] private int _test1Property; - /// - /// Initializes a new instance of the class. - /// - public TestViewModel() - { - Console.Out.WriteLine(Test1Command); - Console.Out.WriteLine(Test2Command); - Console.Out.WriteLine(Test3Command); - Console.Out.WriteLine(Test4Command); - Console.Out.WriteLine(Test5StringToIntCommand); - Console.Out.WriteLine(Test6ArgOnlyCommand); - Console.Out.WriteLine(Test7ObservableCommand); - Console.Out.WriteLine(Test8ObservableCommand); - Console.Out.WriteLine(Test9Command); - Console.Out.WriteLine(Test10Command); - Test1Command?.Execute().Subscribe(); - Test2Command?.Execute().Subscribe(r => Console.Out.WriteLine(r)); - Test3Command?.Execute().Subscribe(); - Test4Command?.Execute().Subscribe(r => Console.Out.WriteLine(r)); - Test5StringToIntCommand?.Execute("100").Subscribe(Console.Out.WriteLine); - Test6ArgOnlyCommand?.Execute("Hello World").Subscribe(); - Test7ObservableCommand?.Execute().Subscribe(); - - _test2PropertyHelper = Test8ObservableCommand!.ToProperty(this, x => x.Test2Property); - - Test8ObservableCommand?.Execute(100).Subscribe(Console.Out.WriteLine); - Console.Out.WriteLine($"Test2Property Value: {Test2}"); - Console.Out.WriteLine($"Test2Property underlying Value: {_test2Property}"); - - Test9Command?.ThrownExceptions.Subscribe(Console.Out.WriteLine); - var cancel = Test9Command?.Execute().Subscribe(); - Task.Delay(1000).Wait(); - cancel?.Dispose(); + /// Gets the initialized MAUI command sample. + public static TestViewModel Instance { get; } = CreateInitializedInstance(); - Test10Command?.Execute(200).Subscribe(r => Console.Out.WriteLine(r)); - } + /// Gets an observable that enables the first sample command. + public IObservable CanExecuteTest1 => Observable.Return(_test1Property >= 0); - /// - /// Gets the instance. - /// - /// - /// The instance. - /// - public static TestViewModel Instance { get; } = new(); - - /// - /// Gets the can execute test1. - /// - /// - /// The can execute test1. - /// -#pragma warning disable CA1822 // Mark members as static - public IObservable CanExecuteTest1 => Observable.Return(true); -#pragma warning restore CA1822 // Mark members as static - - /// - /// Test1s this instance. - /// + /// Writes a message when the first generated command executes. [ReactiveCommand(CanExecute = nameof(CanExecuteTest1))] [property: JsonInclude] - private void Test1() => Console.Out.WriteLine("Test1"); + private static void Test1() => Console.Out.WriteLine("Test1"); - /// - /// Test2s this instance. - /// - /// Rectangle. + /// Returns the default point from the second generated command. + /// The default point value. [ReactiveCommand] - private Point Test2() => default; + private static Point Test2() => default; - /// - /// Test3s the asynchronous. - /// - /// A representing the asynchronous operation. + /// Returns a completed task from the third generated command. + /// A task that completes immediately. [ReactiveCommand] - private async Task Test3Async() => await Task.Delay(0); + private static Task Test3Async() => Task.CompletedTask; - /// - /// Test4s the asynchronous. - /// - /// A representing the asynchronous operation. + /// Returns a point asynchronously from the fourth generated command. + /// A task that produces the configured point. [ReactiveCommand] - private async Task Test4Async() => await Task.FromResult(new Point(100, 100)); + private static Task Test4Async() => Task.FromResult(new Point(PointCoordinate, PointCoordinate)); - /// - /// Test5s the string to int. - /// - /// The string. - /// int. + /// Converts a command string argument into an integer result. + /// The value to convert. + /// The converted integer value. [ReactiveCommand] - private int Test5StringToInt(string str) => int.Parse(str); + private static int Test5StringToInt(string str) => int.Parse(str); - /// - /// Test6s the argument only. - /// - /// The string. + /// Writes the command argument to the console. + /// The value to write. [ReactiveCommand] - private void Test6ArgOnly(string str) => Console.Out.WriteLine($">>> {str}"); + private static void Test6ArgOnly(string str) => Console.Out.WriteLine($">>> {str}"); - /// - /// Test7s the observable. - /// - /// An Observable of Unit. + /// Returns a unit value through an observable command. + /// An observable that returns a unit value. [ReactiveCommand] - private IObservable Test7Observable() => Observable.Return(Unit.Default); + private static IObservable Test7Observable() => Observable.Return(Unit.Default); - /// - /// Test8s the observable. - /// - /// The i. - /// An Observable of int. + /// Returns an offset command value through an observable. + /// The input value to offset. + /// An observable that returns the offset value. [ReactiveCommand] - private IObservable Test8Observable(int i) => Observable.Return(i + 10.0); + private static IObservable Test8Observable(int i) => Observable.Return(i + ObservableCommandOffset); + /// Delays until cancellation is requested by the generated command. + /// The token that cancels the delay. + /// A task that represents the cancellable delay. [ReactiveCommand] - private async Task Test9Async(CancellationToken ct) => await Task.Delay(2000, ct); + private static Task Test9Async(CancellationToken ct) => Task.Delay(CancellableCommandDelayMilliseconds, ct); + /// Returns a point based on the command argument. + /// The width and height of the point. + /// The cancellation token accepted by the command. + /// A task that returns the requested point. [ReactiveCommand] - private async Task Test10Async(int size, CancellationToken ct) => await Task.FromResult(new Point(size, size)); + private static Task Test10Async(int size, CancellationToken ct) => Task.FromResult(new Point(size, size)); + + /// Creates and initializes the singleton after its constructor has completed. + /// The initialized command sample. + private static TestViewModel CreateInitializedInstance() + { + var instance = new TestViewModel(); + instance.InitializeCommandCoverage(); + return instance; + } + + /// Schedules asynchronous disposal of the cancellable command subscription. + /// The command subscription to dispose after the observation delay. + private static void ScheduleCancellation(IDisposable? subscription) + { + if (subscription is null) + { + return; + } + + _ = DisposeAfterDelayAsync(subscription); + } + + /// Disposes a subscription after allowing the command to run briefly. + /// The subscription to dispose. + /// A task that represents the delayed disposal. + private static async Task DisposeAfterDelayAsync(IDisposable subscription) + { + await Task.Delay(CancellationObservationDelayMilliseconds).ConfigureAwait(false); + subscription.Dispose(); + } + + /// Exercises every command shape generated for the MAUI sample. + private void InitializeCommandCoverage() + { + WriteGeneratedCommands(); + ExecuteInitialCommands(); + InitializeObservableProperty(); + ExecuteRemainingCommands(); + } + + /// Writes the generated commands so their creation is visible in the sample output. + private void WriteGeneratedCommands() + { + Console.Out.WriteLine(Test1Command); + Console.Out.WriteLine(Test2Command); + Console.Out.WriteLine(Test3Command); + Console.Out.WriteLine(Test4Command); + Console.Out.WriteLine(Test5StringToIntCommand); + Console.Out.WriteLine(Test6ArgOnlyCommand); + Console.Out.WriteLine(Test7ObservableCommand); + Console.Out.WriteLine(Test8ObservableCommand); + Console.Out.WriteLine(Test9Command); + Console.Out.WriteLine(Test10Command); + } + + /// Executes the generated commands that do not require cancellation. + private void ExecuteInitialCommands() + { + _ = Test1Command?.Execute().Subscribe(); + _ = Test2Command?.Execute().Subscribe(static result => Console.Out.WriteLine(result)); + _ = Test3Command?.Execute().Subscribe(); + _ = Test4Command?.Execute().Subscribe(static result => Console.Out.WriteLine(result)); + _ = Test5StringToIntCommand?.Execute("100").Subscribe(Console.Out.WriteLine); + _ = Test6ArgOnlyCommand?.Execute("Hello World").Subscribe(); + _ = Test7ObservableCommand?.Execute().Subscribe(); + } + + /// Initializes and exercises the observable-as-property sample. + private void InitializeObservableProperty() + { + _test2PropertyHelper = Test8ObservableCommand!.ToProperty(this, static viewModel => viewModel.Test2Property); + _ = Test8ObservableCommand.Execute(ObservableCommandArgument).Subscribe(Console.Out.WriteLine); + Console.Out.WriteLine($"Test2Property Value: {Test2Property}"); + Console.Out.WriteLine($"Test2Property underlying Value: {_test2Property}"); + } + + /// Executes the cancellable and argument-taking generated commands. + private void ExecuteRemainingCommands() + { + _ = Test9Command?.ThrownExceptions.Subscribe(Console.Out.WriteLine); + ScheduleCancellation(Test9Command?.Execute().Subscribe()); + _ = Test10Command?.Execute(PointCommandArgument).Subscribe(static result => Console.Out.WriteLine(result)); + } } diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs b/src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs index eae98498..317cf911 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs @@ -1,20 +1,17 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Execute.Nested1; -/// -/// Class1. -/// +/// Provides the first nested reactive-property sample. [ExcludeFromCodeCoverage] public partial class Class1 : ReactiveObject { + /// Stores the first generated property value. [Reactive] private string? _property1; } diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj index 284cbd8b..0d28f5b5 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -14,7 +14,11 @@ - + + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs b/src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs index 10709365..8209cd1f 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs @@ -1,20 +1,17 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Execute.Nested2; -/// -/// Class1. -/// +/// Provides the second nested reactive-property sample. [ExcludeFromCodeCoverage] public partial class Class1 : ReactiveObject { + /// Stores the second generated property value. [Reactive] private string? _property1; } diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj index 5fa975ec..4ca541f3 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -14,4 +14,8 @@ + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs b/src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs index 6f28f4cf..81d185bf 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs @@ -1,40 +1,48 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; -using ReactiveUI; using ReactiveUI.SourceGenerators; -using SGReactiveUI.SourceGenerators.Execute.Nested2; namespace SGReactiveUI.SourceGenerators.Execute.Nested3; -/// -/// Class1. -/// +/// Provides the third nested reactive-command sample. [ExcludeFromCodeCoverage] public partial class Class1 : ReactiveObject { + /// Stores the third generated property value. [Reactive] private string? _property1; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public Class1() { - SetPropertyCommand.Execute(new Nested1.Class1 { Property1 = "Initial Value" }).Subscribe(); + _ = SetPropertyCommand.Execute(new Nested1.Class1 { Property1 = "Initial Value" }).Subscribe(new CommandObserver()); } + /// Copies the input object to the corresponding second nested object. + /// The object to copy. + /// The copied object, or when the input is null. [ReactiveCommand] - private SGReactiveUI.SourceGenerators.Execute.Nested2.Class1? SetProperty(Nested1.Class1? class1) + private static SGReactiveUI.SourceGenerators.Execute.Nested2.Class1? SetProperty(Nested1.Class1? class1) => class1 is null ? null : new() { Property1 = class1.Property1 }; + + /// Observes results emitted by the generated command. + private sealed class CommandObserver : IObserver { - if (class1 == null) + /// Handles successful command completion. + public void OnCompleted() { - return null; } - return new() { Property1 = class1.Property1 }; + /// Propagates an error emitted by the generated command. + /// The error to propagate. + public void OnError(Exception error) => throw error; + + /// Handles an object emitted by the generated command. + /// The emitted object. + public void OnNext(SGReactiveUI.SourceGenerators.Execute.Nested2.Class1? value) + { + } } } diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested3/Nested4/Class1.cs b/src/ReactiveUI.SourceGenerators.Execute.Nested3/Nested4/Class1.cs index 043c060b..0d588d82 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested3/Nested4/Class1.cs +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested3/Nested4/Class1.cs @@ -1,18 +1,15 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Execute.Nested4; -/// -/// Class1. -/// +/// Provides the fourth nested reactive-property sample. public partial class Class1 : ReactiveObject { + /// Stores the fourth generated property value. [Reactive] private string? _property1; } diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj index 21ad77ab..f264397c 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -14,4 +14,8 @@ + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute/InternalTestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/InternalTestViewModel.cs index 1393c12f..53502b64 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/InternalTestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/InternalTestViewModel.cs @@ -1,42 +1,53 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; +/// Provides reactive-property generation examples with varied access modifiers. [ExcludeFromCodeCoverage] -internal partial class InternalTestViewModel : ReactiveObject +public partial class InternalTestViewModel : ReactiveObject { + /// Represents the first sample collection item. + private const int FirstItemValue = 1; + + /// Represents the second sample collection item. + private const int SecondItemValue = 2; + + /// Represents the third sample collection item. + private const int ThirdItemValue = 3; + + /// Stores the collection used by the reactive collection example. [ReactiveCollection] private ObservableCollection? _publicObservableCollectionTest; + /// Initializes a new instance of the class. public InternalTestViewModel() { // observe property changes - Changed + _ = Changed .Subscribe(x => { // handle property changes - if (x.PropertyName == nameof(PublicObservableCollectionTest)) + if (x.PropertyName != nameof(PublicObservableCollectionTest)) { - Console.WriteLine($"PublicObservableCollectionTest changed: {PublicObservableCollectionTest?.Count}"); + return; } + + System.Diagnostics.Debug.WriteLine($"PublicObservableCollectionTest changed: {PublicObservableCollectionTest?.Count}"); }); PublicObservableCollectionTest = []; - PublicObservableCollectionTest.Add(1); - PublicObservableCollectionTest.Add(2); - PublicObservableCollectionTest.Add(3); - PublicObservableCollectionTest = null; + PublicObservableCollectionTest.Add(FirstItemValue); + PublicObservableCollectionTest.Add(SecondItemValue); + PublicObservableCollectionTest.Add(ThirdItemValue); PublicObservableCollectionTest = []; - PublicObservableCollectionTest.Add(1); - PublicObservableCollectionTest.Add(2); - PublicObservableCollectionTest.Add(3); + PublicObservableCollectionTest.Add(FirstItemValue); + PublicObservableCollectionTest.Add(SecondItemValue); + PublicObservableCollectionTest.Add(ThirdItemValue); } [Reactive] diff --git a/src/ReactiveUI.SourceGenerators.Execute/PLCInstance.cs b/src/ReactiveUI.SourceGenerators.Execute/PLCInstance.cs index 71eec859..ce2190a9 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/PLCInstance.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/PLCInstance.cs @@ -1,33 +1,24 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace SGReactiveUI.SourceGenerators.Test; -/// -/// Represents a programmable logic controller (PLC) instance within the application. -/// +/// Represents a programmable logic controller (PLC) instance within the application. public class PLCInstance { - /// - /// Gets or sets the unique identifier for the PLC instance. - /// + /// Gets or sets the unique identifier for the PLC instance. public int Id { get; set; } - /// - /// Gets or sets the name of the PLC instance. - /// + + /// Gets or sets the name of the PLC instance. public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the IP address of the PLC instance. - /// + + /// Gets or sets the IP address of the PLC instance. public string IPAddress { get; set; } = string.Empty; - /// - /// Gets or sets a value indicating whether the PLC instance is currently active. - /// + + /// Gets or sets a value indicating whether the PLC instance is currently active. public bool IsActive { get; set; } - /// - /// Gets or sets the network port number used for the connection. - /// + + /// Gets or sets the network port number used for the connection. public int Port { get; set; } } diff --git a/src/ReactiveUI.SourceGenerators.Execute/Person.cs b/src/ReactiveUI.SourceGenerators.Execute/Person.cs index f20e1e0f..1710ac7d 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/Person.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/Person.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; @@ -8,10 +7,8 @@ namespace SGReactiveUI.SourceGenerators.Test; -/// -/// Person. -/// -/// +/// Represents a person in the test data. +/// [ExcludeFromCodeCoverage] [IReactiveObject] public partial class Person diff --git a/src/ReactiveUI.SourceGenerators.Execute/Program.cs b/src/ReactiveUI.SourceGenerators.Execute/Program.cs index 3f1950a5..4ce9524f 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/Program.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/Program.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; @@ -9,15 +8,12 @@ namespace SGReactiveUI.SourceGenerators.Test; -/// -/// EntryPoint. -/// +/// Provides the application entry point. [ExcludeFromCodeCoverage] public static class Program { - /// - /// Defines the entry point of the application. - /// + /// Defines the entry point of the application. + [System.STAThread] public static void Main() { AppLocator.CurrentMutable.RegisterViewsForViewModelsSourceGenerated(); diff --git a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj index 4182983e..85dc8229 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,7 +15,8 @@ - + + @@ -24,4 +25,9 @@ + + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestAttribute.cs b/src/ReactiveUI.SourceGenerators.Execute/TestAttribute.cs index 5ad82463..eafc6008 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestAttribute.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestAttribute.cs @@ -1,23 +1,18 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestAttribute. -/// +/// Provides a test attribute for generated property metadata. /// [ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class TestAttribute : Attribute { - /// - /// Gets a parameter. - /// + /// Gets a parameter. /// /// a parameter. /// diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestClassOAPH_VM.cs b/src/ReactiveUI.SourceGenerators.Execute/TestClassOAPH_VM.cs index fd6dbb84..83c47010 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestClassOAPH_VM.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestClassOAPH_VM.cs @@ -1,59 +1,40 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; -using ReactiveUI; +using ReactiveUI.Reactive; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestClassOAPH VM. -/// +/// TestClassOAPH VM. [ExcludeFromCodeCoverage] public partial class TestClassOAPH_VM : ReactiveObject { + /// Stores the observable boolean field. [ObservableAsProperty] private bool _observableTestField; + /// Stores the reactive boolean field. [Reactive] private bool _reactiveTestField; + /// Stores the reactive string value. [Reactive] -#pragma warning disable SX1309 // Field names should begin with underscore - private string value = string.Empty; -#pragma warning restore SX1309 // Field names should begin with underscore + private string _value = string.Empty; + /// Stores the nullable test property. [Reactive] private string? _testProperty; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public TestClassOAPH_VM() { - _observableTestPropertyHelper = this.WhenAnyValue(x => x.ReactiveTestProperty) - .ToProperty(this, x => x.ObservableTestProperty); - - _observableTestFieldHelper = this.WhenAnyValue(x => x.ReactiveTestField) - .ToProperty(this, x => x.ObservableTestField); - - _testHelper = this.WhenAnyValue(x => x.TestProperty).ToProperty(this, x => x.Test); - - TestProperty = null; - var t0 = Test; - - TestProperty = "Test"; - - var t1 = Test; - - TestProperty = null; - var t2 = Test; - + _observableTestPropertyHelper = CreateObservableTestPropertyHelper(); + _observableTestFieldHelper = CreateObservableTestFieldHelper(); + _testHelper = CreateTestHelper(); TestProperty = "Test2"; - var t3 = Test; } /// @@ -82,4 +63,19 @@ public TestClassOAPH_VM() /// [ObservableAsProperty] public partial string? Test { get; } + + /// Creates the helper that projects the reactive property to its observable counterpart. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreateObservableTestPropertyHelper() => + this.WhenAnyValue(x => x.ReactiveTestProperty).ToProperty(this, x => x.ObservableTestProperty); + + /// Creates the helper that projects the reactive field to its observable counterpart. + /// The initialized observable field helper. + private ObservableAsPropertyHelper CreateObservableTestFieldHelper() => + this.WhenAnyValue(x => x.ReactiveTestField).ToProperty(this, x => x.ObservableTestField); + + /// Creates the helper that projects the test property. + /// The initialized test helper. + private ObservableAsPropertyHelper CreateTestHelper() => + this.WhenAnyValue(x => x.TestProperty).ToProperty(this, x => x.Test); } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.Initialization.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.Initialization.cs new file mode 100644 index 00000000..280eabb4 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.Initialization.cs @@ -0,0 +1,164 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive.Linq; +using DynamicData; +using ReactiveUI.Reactive; +using ReactiveUI.SourceGenerators; + +namespace SGReactiveUI.SourceGenerators.Test; + +/// Contains initialization and sample execution helpers for . +public partial class TestViewModel +{ + /// Copies the input nested object to the corresponding output nested object. + /// The nested object to copy. + /// The copied nested object, or when the input is null. + [ReactiveCommand] + private static Execute.Nested2.Class1? SetProperty(Execute.Nested1.Class1? class1) => + class1 is null ? null : new() { Property1 = class1.Property1 }; + + /// Creates the helper that projects the test command output. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreateTest2PropertyHelper() => + Test8ObservableCommand!.ToProperty(this, x => x.Test2Property); + + /// Creates the helper that projects the activated reactive property. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreateTest11PropertyHelper() => + this.WhenAnyValue(x => x.Test12Property).ToProperty(this, x => x.Test11Property, out _); + + /// Creates the helper that projects the protected observable property. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreateObservableAsPropertyTest3PropertyHelper() => + this.WhenAnyValue(x => x.Test13Property).ToProperty(this, x => x.ObservableAsPropertyTest3Property); + + /// Creates the helper that projects the partial observable property. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreateObservableAsPropertyFromPropertyHelper() => + _fromPartialTestSubject.ToProperty(this, x => x.ObservableAsPropertyFromProperty); + + /// Creates the helper that projects the active PLC identifier. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreatePlcActiveHelper() => + this.WhenAnyValue(x => x.PartialRequiredPropertyTest).ToProperty(this, nameof(PLCActive)); + + /// Creates the helper that projects the PLC port. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreatePlcPortHelper() => + this.WhenAnyValue(x => x.Test1Property).ToProperty(this, nameof(PLCPort)); + + /// Creates the helper that projects the PLC instance. + /// The initialized observable property helper. + private ObservableAsPropertyHelper CreatePlcInstanceHelper() => + this.WhenAnyValue(x => x.PlcInstanceCore).ToProperty(this, nameof(InstanceOfPLC)); + + /// Registers the lifecycle activation subscriptions. + private void RegisterActivation() => + this.WhenActivated(disposables => + { + Console.Out.WriteLine("Activated"); + _test11PropertyHelper?.Dispose(); + _test11PropertyHelper = CreateTest11PropertyHelper(); + disposables(_test11PropertyHelper); + disposables(GetDataCommand.Do(static _ => Console.Out.WriteLine("GetDataCommand Executed")).Subscribe()); + disposables(GetDataCommand.Execute().Subscribe()); + }); + + /// Initializes observable property values before sample commands execute. + private void InitializeObservableProperties() + { + Console.Out.WriteLine("MyReadOnlyProperty before init"); + _myReadOnlyProperty = NegativeInitialReadOnlyValue; + Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + Console.Out.WriteLine("MyReadOnlyNonNullProperty before init"); + _myReadOnlyNonNullProperty = NegativeNonNullReadOnlyValue; + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + _observableAsPropertyTest2Property = ObservableAsPropertyInitialValue; + Console.Out.WriteLine(ObservableAsPropertyTest2Property); + Console.Out.WriteLine(_observableAsPropertyTest2Property); + InitializeOAPH(); + } + + /// Exercises the initial generated command set. + private void ExerciseInitialCommands() + { + Console.Out.WriteLine(Test1Command); + Console.Out.WriteLine(Test2Command); + Console.Out.WriteLine(Test3Command); + Console.Out.WriteLine(Test4Command); + Console.Out.WriteLine(Test5StringToIntCommand); + Console.Out.WriteLine(Test6ArgOnlyCommand); + Console.Out.WriteLine(Test7ObservableCommand); + Console.Out.WriteLine(Test8ObservableCommand); + Console.Out.WriteLine(Test9Command); + Console.Out.WriteLine(Test10Command); + Test1Command?.Execute().Subscribe(); + Test2Command?.Execute().Subscribe(static r => Console.Out.WriteLine(r)); + Test3Command?.Execute().Subscribe(); + Test4Command?.Execute().Subscribe(static r => Console.Out.WriteLine(r)); + Test5StringToIntCommand?.Execute("100").Subscribe(Console.Out.WriteLine); + Test6ArgOnlyCommand?.Execute("Hello World").Subscribe(); + Test7ObservableCommand?.Execute().Subscribe(); + Console.Out.WriteLine($"Test2Property default Value: {Test2Property}"); + Test8ObservableCommand?.Execute(CommandArgumentValue).Subscribe(static d => Console.Out.WriteLine(d)); + Console.Out.WriteLine($"Test2Property Value: {Test2Property}"); + Console.Out.WriteLine($"Test2Property underlying Value: {_test2Property}"); + Console.Out.WriteLine(ObservableAsPropertyTest2Property); + } + + /// Exercises the nullable and non-null observable property samples. + private void ExerciseReadOnlyProperties() + { + Console.Out.WriteLine("MyReadOnlyProperty After Init"); + _myReadOnlyProperty = NegativeUpdatedReadOnlyValue; + Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + _testSubject.OnNext(FirstObservedDoubleValue); + Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + _testSubject.OnNext(null); + Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + Console.Out.WriteLine("MyReadOnlyNonNullProperty After Init"); + _myReadOnlyNonNullProperty = NegativeUpdatedReadOnlyValue; + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + _testNonNullSubject.OnNext(SecondObservedDoubleValue); + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + _testNonNullSubject.OnNext(default); + Console.Out.WriteLine(_test13Property); + Console.Out.WriteLine(Test13Property); + Console.Out.WriteLine(_test13PropertyHelper); + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + } + + /// Exercises the remaining commands and observable property updates. + private void ExerciseRemainingCommands() + { + Test9Command?.ThrownExceptions.Subscribe(Console.Out.WriteLine); + var cancel = Test9Command?.Execute().Subscribe(); + cancel?.Dispose(); + Test10Command?.Execute(CommandResultValue).Subscribe(static r => Console.Out.WriteLine(r)); + TestPrivateCanExecuteCommand?.Execute().Subscribe(); + Console.Out.WriteLine($"Observable unset, value should be 10, value is : {ObservableAsPropertyFromProperty}"); + _fromPartialTestSubject.OnNext(ObservableUpdatedValue); + Console.Out.WriteLine($"Observable updated, value should be 11, value is : {ObservableAsPropertyFromProperty}"); + _ = Console.ReadLine(); + } + + /// Registers the subscription that maintains the visible people collection. + private void RegisterPeopleSubscription() => + _ = this.WhenAnyValue(vm => vm.People) + .Subscribe(people => people + .AsObservableChangeSet() + .AutoRefresh(x => x.Deleted) + .Filter(static x => !x.Deleted) + .Bind(out _visiblePeople) + .Subscribe()); +} diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 51f8686d..dd94cbe4 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -1,237 +1,175 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Reactive; using System.Reactive.Concurrency; -using System.Reactive.Disposables.Fluent; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Runtime.Serialization; using System.Text.Json.Serialization; -using DynamicData; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestClass. -/// -/// -/// +/// Provides a comprehensive source-generator execution sample. +/// +/// /// -/// -/// /// [DataContract] public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDisposable { + /// Represents the initial nullable read-only value. + private const double NegativeInitialReadOnlyValue = -1.0D; + + /// Represents the initial non-null read-only value. + private const double NegativeNonNullReadOnlyValue = -5.0D; + + /// Represents the updated read-only value. + private const double NegativeUpdatedReadOnlyValue = -2.0D; + + /// Represents the first observed double value. + private const double FirstObservedDoubleValue = 10.0D; + + /// Represents the second observed double value. + private const double SecondObservedDoubleValue = 11.0D; + + /// Represents the initial observable-as-property value. + private const int ObservableAsPropertyInitialValue = 11_223_344; + + /// Represents the argument passed to the sample command. + private const int CommandArgumentValue = 100; + + /// Represents the expected sample command result. + private const int CommandResultValue = 200; + + /// Represents the value published after the observable updates. + private const int ObservableUpdatedValue = 11; + + /// Represents the default observable value. + private const int DefaultObservableValue = 9; + + /// Represents the offset applied by the observable command. + private const double ObservableCommandOffset = 10.0D; + + /// Represents the cancellation delay in milliseconds. + private const int CancellationDelayMilliseconds = 2_000; + + /// Provides the observable used to control the private command. private readonly IObservable _observable = Observable.Return(true); + + /// Publishes nullable values for the observable-as-property example. private readonly Subject _testSubject = new(); + + /// Publishes non-null values for the observable-as-property example. private readonly Subject _testNonNullSubject = new(); + + /// Publishes values for the partial observable-as-property example. private readonly Subject _fromPartialTestSubject = new(); + + /// Provides the scheduler used by generated reactive commands. private readonly IScheduler _scheduler = RxSchedulers.MainThreadScheduler; + /// Stores the first observable-as-property value. [property: JsonInclude] [DataMember] [ObservableAsProperty] - private double? _test2Property = 1.1d; + private double? _test2Property = 1.1D; + /// Stores the second observable-as-property value. [ObservableAsProperty(ReadOnly = false)] - private double? _test11Property = 11.1d; + private double? _test11Property = 11.1D; + /// Stores the third observable-as-property value. [ObservableAsProperty(ReadOnly = false)] - private double _test13Property = 11.1d; + private double _test13Property = 11.1D; + /// Stores the protected observable-as-property value. [ObservableAsProperty(UseProtected = true)] private double _observableAsPropertyTest3Property; + /// Stores the value used by the reactive test property. [property: Test(AParameter = "Test Input")] [Reactive] - private double? _test12Property = 12.1d; + private double? _test12Property = 12.1D; + /// Stores the protected reactive test property value. [Reactive(SetModifier = AccessModifier.Protected)] [property: JsonInclude] [DataMember] private int _test1Property; + + /// Tracks whether the instance has disposed its resources. private bool _disposedValue; + /// Stores the mutable string test value. [Reactive] private string _myStringProperty = "test"; + /// Stores the nullable reactive name. [property: JsonInclude] [DataMember] [Reactive(Inheritance = InheritanceModifier.Virtual, SetModifier = AccessModifier.Protected)] private string? _name; + /// Stores the required reactive value. [Reactive(nameof(MyDoubleProperty), nameof(MyStringProperty), SetModifier = AccessModifier.Init, UseRequired = true)] private string _mustBeSet; + /// Stores the people included in the derived list example. [Reactive] private IEnumerable _people = [new Person()]; + + /// Stores the nullable double reactive value. [Reactive] private double? _myDoubleProperty; + + /// Stores the non-null double reactive value. [Reactive] private double _myDoubleNonNullProperty; + /// Stores the PLC instance used by the reactive property example. [Reactive] private PLCInstance _plcInstanceCore = new(); + /// Stores the derived observable collection of visible people. [BindableDerivedList] private ReadOnlyObservableCollection? _visiblePeople; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. [SetsRequiredMembers] public TestViewModel() { - var itv = new InternalTestViewModel { PublicRequiredPartialPropertyTest = true }; + _ = new InternalTestViewModel { PublicRequiredPartialPropertyTest = true }; MustBeSet = "Test"; - this.WhenActivated(disposables => - { - Console.Out.WriteLine("Activated"); - _test11PropertyHelper = this.WhenAnyValue(x => x.Test12Property).ToProperty(this, x => x.Test11Property, out _).DisposeWith(disposables); - GetDataCommand.Do(_ => Console.Out.WriteLine("GetDataCommand Executed")).Subscribe().DisposeWith(disposables); - GetDataCommand.Execute().Subscribe().DisposeWith(disposables); - }); - - Console.Out.WriteLine("MyReadOnlyProperty before init"); - - // only settable prior to init, after init it will be ignored. - _myReadOnlyProperty = -1.0; - Console.Out.WriteLine(MyReadOnlyProperty); - Console.Out.WriteLine(_myReadOnlyProperty); - - Console.Out.WriteLine("MyReadOnlyNonNullProperty before init"); - - // only settable prior to init, after init it will be ignored. - _myReadOnlyNonNullProperty = -5.0; - Console.Out.WriteLine(MyReadOnlyNonNullProperty); - Console.Out.WriteLine(_myReadOnlyNonNullProperty); - - _observableAsPropertyTest2Property = 11223344; - Console.Out.WriteLine(ObservableAsPropertyTest2Property); - Console.Out.WriteLine(_observableAsPropertyTest2Property); - + _test11PropertyHelper = CreateTest11PropertyHelper(); + _test2PropertyHelper = CreateTest2PropertyHelper(); + _observableAsPropertyTest3PropertyHelper = CreateObservableAsPropertyTest3PropertyHelper(); + _observableAsPropertyFromPropertyHelper = CreateObservableAsPropertyFromPropertyHelper(); + _pLCActiveHelper = CreatePlcActiveHelper(); + _pLCPortHelper = CreatePlcPortHelper(); + _instanceOfPLCHelper = CreatePlcInstanceHelper(); _referenceTypeObservableProperty = default!; ReferenceTypeObservable = Observable.Return(new object()); NullableReferenceTypeObservable = Observable.Return(new object()); - - InitializeOAPH(); - - Console.Out.WriteLine(Test1Command); - Console.Out.WriteLine(Test2Command); - Console.Out.WriteLine(Test3Command); - Console.Out.WriteLine(Test4Command); - Console.Out.WriteLine(Test5StringToIntCommand); - Console.Out.WriteLine(Test6ArgOnlyCommand); - Console.Out.WriteLine(Test7ObservableCommand); - Console.Out.WriteLine(Test8ObservableCommand); - Console.Out.WriteLine(Test9Command); - Console.Out.WriteLine(Test10Command); - Test1Command?.Execute().Subscribe(); - Test2Command?.Execute().Subscribe(r => Console.Out.WriteLine(r)); - Test3Command?.Execute().Subscribe(); - Test4Command?.Execute().Subscribe(r => Console.Out.WriteLine(r)); - Test5StringToIntCommand?.Execute("100").Subscribe(Console.Out.WriteLine); - Test6ArgOnlyCommand?.Execute("Hello World").Subscribe(); - Test7ObservableCommand?.Execute().Subscribe(); - - Console.Out.WriteLine($"Test2Property default Value: {Test2Property}"); - _test2PropertyHelper = Test8ObservableCommand!.ToProperty(this, x => x.Test2Property); - _observableAsPropertyTest3PropertyHelper = this.WhenAnyValue(x => x.Test13Property)!.ToProperty(this, x => x.ObservableAsPropertyTest3Property); - - Test8ObservableCommand?.Execute(100).Subscribe(d => Console.Out.WriteLine(d)); - Console.Out.WriteLine($"Test2Property Value: {Test2Property}"); - Console.Out.WriteLine($"Test2Property underlying Value: {_test2Property}"); - Console.Out.WriteLine(ObservableAsPropertyTest2Property); - - Console.Out.WriteLine("MyReadOnlyProperty After Init"); - - // setting this value should not update the _myReadOnlyPropertyHelper as the _testSubject has not been updated yet but the _myReadOnlyPropertyHelper should be updated with null upon init. - _myReadOnlyProperty = -2.0; - - // null value expected as the _testSubject has not been updated yet, ignoring the private variable. - Console.Out.WriteLine(MyReadOnlyProperty); - Console.Out.WriteLine(_myReadOnlyProperty); - _testSubject.OnNext(10.0); - - // expected value 10 as the _testSubject has been updated. - Console.Out.WriteLine(MyReadOnlyProperty); - Console.Out.WriteLine(_myReadOnlyProperty); - _testSubject.OnNext(null); - - // expected value null as the _testSubject has been updated. - Console.Out.WriteLine(MyReadOnlyProperty); - Console.Out.WriteLine(_myReadOnlyProperty); - - Console.Out.WriteLine("MyReadOnlyNonNullProperty After Init"); - - // setting this value should not update the _myReadOnlyNonNullProperty as the _testNonNullSubject has not been updated yet but the _myReadOnlyNonNullPropertyHelper should be updated with null upon init. - _myReadOnlyNonNullProperty = -2.0; - - // 0 value expected as the _testNonNullSubject has not been updated yet, ignoring the private variable. - Console.Out.WriteLine(MyReadOnlyNonNullProperty); - Console.Out.WriteLine(_myReadOnlyNonNullProperty); - _testNonNullSubject.OnNext(11.0); - - // expected value 11 as the _testNonNullSubject has been updated. - Console.Out.WriteLine(MyReadOnlyNonNullProperty); - Console.Out.WriteLine(_myReadOnlyNonNullProperty); - _testNonNullSubject.OnNext(default); - - Console.Out.WriteLine(_test13Property); - Console.Out.WriteLine(Test13Property); - Console.Out.WriteLine(_test13PropertyHelper); - - // expected value 0 as the _testNonNullSubject has been updated. - Console.Out.WriteLine(MyReadOnlyNonNullProperty); - Console.Out.WriteLine(_myReadOnlyNonNullProperty); - - Test9Command?.ThrownExceptions.Subscribe(Console.Out.WriteLine); - var cancel = Test9Command?.Execute().Subscribe(); - Task.Delay(1000).Wait(); - cancel?.Dispose(); - - Test10Command?.Execute(200).Subscribe(r => Console.Out.WriteLine(r)); - TestPrivateCanExecuteCommand?.Execute().Subscribe(); - - Console.Out.WriteLine($"Observable unset, value should be 10, value is : {ObservableAsPropertyFromProperty}"); - _observableAsPropertyFromPropertyHelper = _fromPartialTestSubject.ToProperty(this, x => x.ObservableAsPropertyFromProperty); - _fromPartialTestSubject.OnNext(11); - Console.Out.WriteLine($"Observable updated, value should be 11, value is : {ObservableAsPropertyFromProperty}"); - - this.WhenAnyValue(vm => vm.People) - .Subscribe(people => people - .AsObservableChangeSet() - .AutoRefresh(x => x.Deleted) - .Filter(x => !x.Deleted) - .Bind(out _visiblePeople) - .Subscribe()); - - _pLCActiveHelper = this.WhenAnyValue(x => x.PartialRequiredPropertyTest).ToProperty(this, nameof(PLCActive)); - _pLCPortHelper = this.WhenAnyValue(x => x.Test1Property).ToProperty(this, nameof(PLCPort)); - _instanceOfPLCHelper = this.WhenAnyValue(x => x.PlcInstanceCore).ToProperty(this, nameof(InstanceOfPLC)); - - Console.ReadLine(); + RegisterActivation(); + InitializeObservableProperties(); + ExerciseInitialCommands(); + ExerciseReadOnlyProperties(); + ExerciseRemainingCommands(); + RegisterPeopleSubscription(); } - /// - /// Gets the instance. - /// + /// Gets the instance. /// /// The instance. /// public static TestViewModel Instance { get; } = new(); - /// - /// Gets the test class oaph vm. - /// + /// Gets the test class oaph vm. /// /// The test class oaph vm. /// @@ -257,74 +195,58 @@ public TestViewModel() [Reactive(UseRequired = true)] public required partial string? PartialRequiredPropertyTest { get; set; } - /// - /// Gets the internal test property. Should not prompt to replace with INPC Reactive Property. - /// + /// Gets the internal test property. Should not prompt to replace with INPC Reactive Property. /// /// The test property. /// [JsonInclude] public string? TestInternalSetProperty { get; internal set; } = "Test"; - /// - /// Gets the test private set property. Should not prompt to replace with INPC Reactive Property. - /// + /// Gets the test private set property. Should not prompt to replace with INPC Reactive Property. /// /// The test private set property. /// [JsonInclude] public string? TestPrivateSetProperty { get; private set; } = "Test"; - /// - /// Gets or sets the test automatic property. - /// + /// Gets or sets the test automatic property. /// /// The test automatic property. /// [JsonInclude] public string? TestAutoProperty { get; set; } = "Test, should prompt to replace with INPC Reactive Property"; - /// - /// Gets the test read only property. - /// + /// Gets the test read only property. /// /// The test read only property. /// public string? TestReadOnlyProperty { get; } = "Test, should not prompt to replace with INPC Reactive Property"; - /// - /// Gets or sets the reactive command test property. Should not prompt to replace with INPC Reactive Property. - /// + /// Gets or sets the reactive command test property. Should not prompt to replace with INPC Reactive Property. /// /// The reactive command test property. /// public ReactiveCommand? ReactiveCommandTestProperty { get; set; } - /// - /// Gets or sets the reactive property test property. Should not prompt to replace with INPC Reactive Property. - /// + /// Gets or sets the reactive property test property. Should not prompt to replace with INPC Reactive Property. /// /// The reactive property test property. /// public ReactiveProperty? ReactivePropertyTestProperty { get; set; } - /// - /// Gets the can execute test1. - /// + /// Gets the can execute test1. /// /// The can execute test1. /// - public IObservable CanExecuteTest1 => ObservableAsPropertyTest2.Select(x => x > 0); + public IObservable CanExecuteTest1 => ObservableAsPropertyTest2.Select(static x => x > 0); - /// - /// Gets the observable as property test2. - /// + /// Gets the observable as property test2. /// /// The observable as property test2. /// [ObservableAsProperty] [property: Test(AParameter = "Test Input")] - public IObservable ObservableAsPropertyTest2 => Observable.Return(9); + public IObservable ObservableAsPropertyTest2 => Observable.Return(DefaultObservableValue); /// /// Gets the current active PLC identifier or name. @@ -344,9 +266,7 @@ public TestViewModel() [ObservableAsProperty(InitialValue = $"new {nameof(PLCInstance)}()")] public partial PLCInstance InstanceOfPLC { get; } - /// - /// Gets the Activator which will be used by the View when Activation/Deactivation occurs. - /// + /// Gets the Activator which will be used by the View when Activation/Deactivation occurs. public ViewModelActivator Activator { get; } = new(); /// @@ -364,31 +284,27 @@ public TestViewModel() [Reactive] internal partial int InternalPartialPropertyTest { get; set; } + /// Gets the observable used for the non-null reference type example. [ObservableAsProperty] private IObservable ReferenceTypeObservable { get; } + /// Gets the observable used for the nullable reference type example. [ObservableAsProperty] private IObservable NullableReferenceTypeObservable { get; } - /// - /// Gets observables as property test. - /// + /// Gets observables as property test. /// /// Observable of double. /// [ObservableAsProperty(PropertyName = "MyReadOnlyProperty")] public IObservable ObservableAsPropertyTest() => _testSubject; - /// - /// Observables as property test non null. - /// + /// Observables as property test non null. /// Observable of double. [ObservableAsProperty(PropertyName = "MyReadOnlyNonNullProperty")] public IObservable ObservableAsPropertyTestNonNull() => _testNonNullSubject; - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method @@ -396,98 +312,102 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Releases unmanaged and - optionally - managed resources. - /// + /// Releases unmanaged and - optionally - managed resources. /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (_disposedValue) + { + return; + } + + if (disposing) { - if (disposing) - { - _testSubject.Dispose(); - _testNonNullSubject.Dispose(); - _fromPartialTestSubject.Dispose(); - } - - _disposedValue = true; + _testSubject.Dispose(); + _testNonNullSubject.Dispose(); + _fromPartialTestSubject.Dispose(); } + + _disposedValue = true; } - /// - /// Test1s this instance. - /// + /// Associates a sample result with this view-model instance. + /// The type of the sample result. + /// The result to return. + /// The supplied result. + private T UseInstance(T value) + { + GC.KeepAlive(this); + return value; + } + + /// Writes a sample command message while retaining the owning view model. + /// The message to write. + private void WriteSampleMessage(string message) + { + GC.KeepAlive(this); + Console.Out.WriteLine(message); + } + + /// Test1s this instance. [ReactiveCommand(CanExecute = nameof(CanExecuteTest1))] [property: JsonInclude] [property: Test(AParameter = "Test Input")] - private void Test1() => Console.Out.WriteLine("Test1 Command Executed"); + private void Test1() => WriteSampleMessage("Test1 Command Executed"); - /// - /// Test3s the asynchronous. - /// + /// Test3s the asynchronous. /// A representing the asynchronous operation. [ReactiveCommand] - private async Task Test3Async() => await Task.Delay(0); + private Task Test3Async() => UseInstance(Task.Delay(0)); - /// - /// Test4s the asynchronous. - /// + /// Test4s the asynchronous. /// A representing the asynchronous operation. [ReactiveCommand] - private async Task Test4Async() => await Task.FromResult(new Point(100, 100)); + private Task Test4Async() => UseInstance(Task.FromResult(new Point(CommandArgumentValue, CommandArgumentValue))); - /// - /// Test5s the string to int. - /// + /// Test5s the string to int. /// The string. /// int. [ReactiveCommand] - private int Test5StringToInt(string str) => int.Parse(str); + private int Test5StringToInt(string str) => UseInstance(int.Parse(str)); - /// - /// Test6s the argument only. - /// + /// Test6s the argument only. /// The string. [ReactiveCommand] - private void Test6ArgOnly(string str) => Console.Out.WriteLine($">>> {str}"); + private void Test6ArgOnly(string str) => WriteSampleMessage($">>> {str}"); - /// - /// Test7s the observable. - /// + /// Test7s the observable. /// An Observable of Unit. [ReactiveCommand] - private IObservable Test7Observable() => Observable.Return(Unit.Default); + private IObservable Test7Observable() => UseInstance(Observable.Return(Unit.Default)); - /// - /// Test8s the observable. - /// + /// Test8s the observable. /// The i. /// An Observable of int. [ReactiveCommand(AccessModifier = PropertyAccessModifier.Internal)] - private IObservable Test8Observable(int i) => Observable.Return(i + 10.0); + private IObservable Test8Observable(int i) => UseInstance(Observable.Return(i + ObservableCommandOffset)); + /// Executes the cancellable reactive command sample. + /// The cancellation token for the command. + /// A task that completes when the command finishes. [ReactiveCommand] - private async Task Test9Async(CancellationToken ct) => await Task.Delay(2000, ct); + private Task Test9Async(CancellationToken ct) => UseInstance(Task.Delay(CancellationDelayMilliseconds, ct)); + /// Executes the parameterized cancellable reactive command sample. + /// The size of the generated point. + /// The cancellation token for the command. + /// A task that produces the generated point. [ReactiveCommand] - private async Task Test10Async(int size, CancellationToken ct) => await Task.FromResult(new Point(size, size)); + private Task Test10Async(int size, CancellationToken ct) => UseInstance(Task.FromResult(new Point(size, size))); + /// Executes the command with a private observable can-execute source. [ReactiveCommand(CanExecute = nameof(_observable), OutputScheduler = nameof(_scheduler))] - private void TestPrivateCanExecute() => Console.Out.WriteLine("TestPrivateCanExecute"); + private void TestPrivateCanExecute() => WriteSampleMessage("TestPrivateCanExecute"); + /// Retrieves the empty data sequence used by the reactive command sample. + /// The cancellation token for the command. + /// A task that produces an empty data sequence. [ReactiveCommand] private Task GetData(CancellationToken ct) => - Task.FromResult(Array.Empty()); - - [ReactiveCommand] - private Execute.Nested2.Class1? SetProperty(Execute.Nested1.Class1? class1) - { - if (class1 == null) - { - return null; - } - - return new() { Property1 = class1.Property1 }; - } + UseInstance(Task.FromResult(Array.Empty())); } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel2.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel2.cs index 6e5deec6..8b63c176 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel2.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel2.cs @@ -1,22 +1,19 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestViewModel2. -/// +/// Provides a generic reactive view-model sample. /// the type. -/// +/// [ExcludeFromCodeCoverage] public partial class TestViewModel2 : ReactiveObject { + /// Stores whether the sample state is true. [Reactive] - private bool _IsTrue; + private bool _isTrue; } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel3.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel3.cs index a2201821..823fa89f 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel3.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel3.cs @@ -1,72 +1,81 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestViewModel3. -/// +/// Provides nested reactive command generation examples. [ExcludeFromCodeCoverage] public partial class TestViewModel3 : ReactiveObject { + /// Represents the default generated command result. + private const int CommandResult = 10; + + /// Stores the first generated reactive property value. [Reactive] private float _testVM3Property; + /// Stores the second generated reactive property value. [Reactive] private float _testVM3Property2; + /// Returns the generated command result. + /// The generated command result. [ReactiveCommand] - private int Test1() => 10; + private int Test1() => CommandResult + (int)_testVM3Property; - /// - /// TestInnerClass. - /// + /// Provides the first nested reactive command generation example. public partial class TestInnerClass1 : ReactiveObject { + /// Stores the first nested reactive property value. [Reactive] private int _testInner1; + /// Stores the second nested reactive property value. [Reactive] private int _testInner11; + /// Returns the first nested generated command result. + /// The first nested generated command result. [ReactiveCommand] - private int TestI1() => 10; + private int TestI1() => CommandResult + _testInner1; } - /// - /// TestInnerClass. - /// + /// Provides the second nested reactive command generation example. public partial class TestInnerClass2 : ReactiveObject { + /// Stores the first nested reactive property value. [Reactive] private int _testInner2; + /// Stores the second nested reactive property value. [Reactive] private int _testInner22; + /// Returns the second nested generated command result. + /// The second nested generated command result. [ReactiveCommand] - private int TestI2() => 10; + private int TestI2() => CommandResult + _testInner2; - /// - /// TestInnerClass4. - /// - /// + /// Provides the third nested reactive command generation example. + /// public partial class TestInnerClass3 : ReactiveObject { + /// Stores the first deeply nested reactive property value. [Reactive] private int _testInner3; + /// Stores the second deeply nested reactive property value. [Reactive] private int _testInner33; + /// Returns the deeply nested generated command result. + /// The deeply nested generated command result. [ReactiveCommand] - private int TestI3() => 10; + private int TestI3() => CommandResult + _testInner3; } } } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs index 1b712e4d..9f2cbd67 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs @@ -1,25 +1,19 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; using ReactiveUI.SourceGenerators; -namespace SGReactiveUI.SourceGenerators.Test +namespace SGReactiveUI.SourceGenerators.Test; + +/// Provides a secondary partial view-model command sample. +/// +[ExcludeFromCodeCoverage] +public partial class TestViewModel { - /// - /// TestViewModel. - /// - /// - [ExcludeFromCodeCoverage] - public partial class TestViewModel - { - /// - /// Test2s this instance. - /// - /// Rectangle. - [ReactiveCommand] - private Point Test2() => default; - } + /// Returns the secondary generated command value. + /// The generated point. + [ReactiveCommand] + private static Point Test2() => default; } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.Designer.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.Designer.cs index 7ac395ce..31ca66a2 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.Designer.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.Designer.cs @@ -1,4 +1,4 @@ -namespace SGReactiveUI.SourceGenerators.Test +namespace SGReactiveUI.SourceGenerators.Test { partial class TestViewWinForms { diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs index fea49856..becab708 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs @@ -1,27 +1,21 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators; -namespace SGReactiveUI.SourceGenerators.Test +namespace SGReactiveUI.SourceGenerators.Test; + +/// Provides the Windows Forms test view. +/// +[IViewFor(RegistrationType = SplatRegistrationType.LazySingleton)] +public partial class TestViewWinForms : Form { - /// - /// TestViewWinForms. - /// - /// - [IViewFor(RegistrationType = SplatRegistrationType.LazySingleton)] - public partial class TestViewWinForms : Form + /// Initializes a new instance of the class. + public TestViewWinForms() { - /// - /// Initializes a new instance of the class. - /// - public TestViewWinForms() - { - InitializeComponent(); - ViewModel = TestViewModel.Instance; - ViewModel.Activator.Activate(); - } + InitializeComponent(); + ViewModel = TestViewModel.Instance; + _ = ViewModel.Activator.Activate(); } } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs index 98c0979f..73cde80e 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs @@ -1,9 +1,7 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Diagnostics.CodeAnalysis; using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -11,24 +9,18 @@ namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestView. -/// +/// Provides the WPF test view. [IViewFor(RegistrationType = SplatRegistrationType.PerRequest, ViewModelRegistrationType = SplatRegistrationType.Constant)] public partial class TestViewWpf : Window { - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public TestViewWpf() { - AppLocator.CurrentMutable.RegisterLazySingleton>(() => new TestViewWpf()); + AppLocator.CurrentMutable.RegisterLazySingleton>(static () => new TestViewWpf()); ViewModel = TestViewModel.Instance; } - /// - /// Gets or sets the test property. - /// + /// Gets or sets the test property. /// /// The test property. /// diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs index 06efcefa..4c342a0e 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs @@ -1,23 +1,17 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Diagnostics.CodeAnalysis; using System.Windows; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestViewWpf2. -/// +/// Provides the generic WPF test view. /// [IViewFor("SGReactiveUI.SourceGenerators.Test.TestViewModel2", RegistrationType = SplatRegistrationType.PerRequest)] public partial class TestViewWpf2 : Window { - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public TestViewWpf2() => ViewModel = new(); } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs index 89b5aaec..16a5ee07 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs @@ -1,4 +1,4 @@ -namespace SGReactiveUI.SourceGenerators.Test; +namespace SGReactiveUI.SourceGenerators.Test; partial class TestWinFormsRCHost { diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs index dd75bbae..421327a5 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Diagnostics.CodeAnalysis; using ReactiveUI.SourceGenerators.WinForms; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestWinFormsRCHost. -/// +/// Provides the routed Windows Forms host generation sample. [RoutedControlHost(nameof(UserControl))] -public partial class TestWinFormsRCHost; +public partial class TestWinFormsRCHost +{ + /// Gets a value indicating whether the routed host sample is available. + internal static bool IsSampleAvailable => true; +} diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.Designer.cs b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.Designer.cs index 068e5a41..5b051b15 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.Designer.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.Designer.cs @@ -1,4 +1,4 @@ -namespace SGReactiveUI.SourceGenerators.Test +namespace SGReactiveUI.SourceGenerators.Test { partial class TestWinFormsVMCHost { diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs index 16538504..20e4d3e7 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs @@ -1,18 +1,18 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Diagnostics.CodeAnalysis; using ReactiveUI.SourceGenerators.WinForms; namespace SGReactiveUI.SourceGenerators.Test; -/// -/// TestWinFormsVMCHost. -/// +/// Provides the view-model Windows Forms host generation sample. /// /// /// [ViewModelControlHost(nameof(UserControl))] -public partial class TestWinFormsVMCHost; +public partial class TestWinFormsVMCHost +{ + /// Gets a value indicating whether the view-model host sample is available. + internal static bool IsSampleAvailable => true; +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.Execute.cs index a7b037e8..9db694e9 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.Execute.cs @@ -1,11 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Immutable; -using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -17,14 +16,31 @@ namespace ReactiveUI.SourceGenerators; -/// -/// BindableDerivedListGenerator. -/// +/// Implements BindableDerivedList source generation. public sealed partial class BindableDerivedListGenerator { + /// Gets the generator type name used in generated-code metadata. internal static readonly string GeneratorName = typeof(BindableDerivedListGenerator).FullName!; + + /// Gets the generator assembly version used in generated-code metadata. internal static readonly string GeneratorVersion = typeof(BindableDerivedListGenerator).Assembly.GetName().Version.ToString(); + /// Represents the internal accessibility option. + private const int InternalAccessModifier = 2; + + /// Represents the private accessibility option. + private const int PrivateAccessModifier = 3; + + /// Represents the protected-internal accessibility option. + private const int ProtectedInternalAccessModifier = 4; + + /// Represents the private-protected accessibility option. + private const int PrivateProtectedAccessModifier = 5; + + /// Creates metadata for a BindableDerivedList-annotated field. + /// The attribute syntax context for the field. + /// The cancellation token for the generator operation. + /// The field metadata and diagnostics, or when not applicable. private static Result? GetVariableInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { using var builder = ImmutableArrayBuilder.Rent(); @@ -42,10 +58,6 @@ public sealed partial class BindableDerivedListGenerator token.ThrowIfCancellationRequested(); - // Get the property type and name - var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - - // Check that the type is a ReadOnlyObservableCollection if (!fieldSymbol.Type.HasOrInheritsFromFullyQualifiedMetadataNameStartingWith("System.Collections.ObjectModel.ReadOnlyObservableCollection")) { builder.Add( @@ -58,6 +70,25 @@ public sealed partial class BindableDerivedListGenerator token.ThrowIfCancellationRequested(); + return CreateResult(context, fieldSymbol, attributeData, builder, token); + } + + /// Creates metadata for a validated BindableDerivedList field. + /// The attribute syntax context for the field. + /// The validated field symbol. + /// The BindableDerivedList attribute data. + /// The diagnostics collected while processing the field. + /// The cancellation token for the generator operation. + /// The field metadata and diagnostics. + private static Result CreateResult( + in GeneratorAttributeSyntaxContext context, + IFieldSymbol fieldSymbol, + AttributeData attributeData, + ImmutableArrayBuilder builder, + CancellationToken token) + { + var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + var fieldName = fieldSymbol.Name; var propertyName = fieldSymbol.GetGeneratedPropertyName(); @@ -73,24 +104,12 @@ public sealed partial class BindableDerivedListGenerator token.ThrowIfCancellationRequested(); - // Get AccessModifier enum value from the attribute - var accessModifier = attributeData.GetNamedArgument("AccessModifier") switch - { - 1 => "protected", - 2 => "internal", - 3 => "private", - 4 => "protected internal", - 5 => "private protected", - _ => "public", - }; + var accessModifier = GetAccessModifier(attributeData); token.ThrowIfCancellationRequested(); - // Get the nullability info for the property - fieldSymbol.GetNullabilityInfo( - context.SemanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); + var (isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor) = + GetNullabilityInfo(fieldSymbol, context.SemanticModel); token.ThrowIfCancellationRequested(); var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; @@ -122,12 +141,53 @@ public sealed partial class BindableDerivedListGenerator builder.ToImmutable()); } - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, BindableDerivedListInfo[] properties) + /// Gets the generated accessibility text from the attribute configuration. + /// The BindableDerivedList attribute data. + /// The generated accessibility text. + private static string GetAccessModifier(AttributeData attributeData) => + attributeData.GetNamedArgument("AccessModifier") switch + { + 1 => "protected", + InternalAccessModifier => "internal", + PrivateAccessModifier => "private", + ProtectedInternalAccessModifier => "protected internal", + PrivateProtectedAccessModifier => "private protected", + _ => "public", + }; + + /// Gets nullability metadata for a generated BindableDerivedList property. + /// The source field symbol. + /// The semantic model for the source field. + /// The reference-type and member-not-null metadata. + private static (bool IsReferenceType, bool IncludeMemberNotNull) GetNullabilityInfo( + IFieldSymbol fieldSymbol, + SemanticModel semanticModel) + { + fieldSymbol.GetNullabilityInfo( + semanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); + return (isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor); + } + + /// Generates the complete source document for a BindableDerivedList declaration. + /// The name of the containing type. + /// The containing namespace. + /// The containing type visibility. + /// The containing type kind. + /// The generated property metadata. + /// The generated source document. + private static string GenerateSource( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + BindableDerivedListInfo[] properties) { // Get Parent class details from properties.ParentInfo - var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(properties.Select(p => p.TargetInfo.ParentInfo).ToArray()); + var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(GetParentInfos(properties)); - var classes = GenerateClassWithProperties(containingTypeName, containingNamespace, containingClassVisibility, containingType, properties); + var classes = GenerateClassWithProperties(containingTypeName, containingClassVisibility, containingType, properties); return $$""" @@ -148,19 +208,20 @@ namespace {{containingNamespace}} """; } - /// - /// Generates the source code. - /// + /// Generates the source code. /// The contain type name. - /// The containing namespace. /// The containing class visibility. /// The containing type. /// The properties. /// The value. - private static string GenerateClassWithProperties(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, BindableDerivedListInfo[] properties) + private static string GenerateClassWithProperties( + string containingTypeName, + string containingClassVisibility, + string containingType, + BindableDerivedListInfo[] properties) { // Includes 2 tabs from the property declarations so no need to add them here. - var propertyDeclarations = string.Join("\n", properties.Select(GetPropertySyntax)); + var propertyDeclarations = GetPropertyDeclarations(properties); return $$""" @@ -173,9 +234,7 @@ private static string GenerateClassWithProperties(string containingTypeName, str """; } - /// - /// Generates property declarations for the given observable method information. - /// + /// Generates property declarations for the given observable method information. /// Metadata about the observable property. /// A string containing the generated code for the property. private static string GetPropertySyntax(BindableDerivedListInfo propertyInfo) @@ -185,7 +244,7 @@ private static string GetPropertySyntax(BindableDerivedListInfo propertyInfo) return string.Empty; } - var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); + var propertyAttributes = GetPropertyAttributes(propertyInfo); return $$""" @@ -194,4 +253,69 @@ private static string GetPropertySyntax(BindableDerivedListInfo propertyInfo) {{propertyInfo.AccessModifier}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} => {{propertyInfo.FieldName}}; """; } + + /// Gets parent information for each generated property. + /// The generated property metadata. + /// The parent information for the generated properties. + private static TargetInfo?[] GetParentInfos(BindableDerivedListInfo[] properties) + { + var parentInfos = new TargetInfo?[properties.Length]; + for (var index = 0; index < properties.Length; index++) + { + parentInfos[index] = properties[index].TargetInfo.ParentInfo; + } + + return parentInfos; + } + + /// Gets the generated property declarations. + /// The generated property metadata. + /// The generated property declarations. + private static string GetPropertyDeclarations(BindableDerivedListInfo[] properties) + { + var builder = new StringBuilder(); + for (var index = 0; index < properties.Length; index++) + { + if (index > 0) + { + _ = builder.Append('\n'); + } + + _ = builder.Append(GetPropertySyntax(properties[index])); + } + + return builder.ToString(); + } + + /// Gets attributes applied to a generated property. + /// The source field metadata. + /// The property attributes separated by generated-code indentation. + private static string GetPropertyAttributes(BindableDerivedListInfo propertyInfo) + { + var builder = new StringBuilder(); + foreach (var attribute in AttributeDefinitions.ExcludeFromCodeCoverage) + { + AppendPropertyAttribute(builder, attribute); + } + + foreach (var attribute in propertyInfo.ForwardedAttributes.AsImmutableArray()) + { + AppendPropertyAttribute(builder, attribute); + } + + return builder.ToString(); + } + + /// Appends a generated property attribute with the required separator. + /// The destination builder. + /// The attribute source. + private static void AppendPropertyAttribute(StringBuilder builder, string attribute) + { + if (builder.Length > 0) + { + _ = builder.Append("\n "); + } + + _ = builder.Append(attribute); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs index 1edc983d..2d638e54 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs @@ -1,28 +1,25 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.BindableDerivedList.Models; using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating BindableDerivedList properties. -/// +/// A source generator for generating BindableDerivedList properties. [Generator(LanguageNames.CSharp)] public sealed partial class BindableDerivedListGenerator : IIncrementalGenerator { /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => { // Add the BindableDerivedListAttribute to the compilation ctx.AddSource($"{AttributeDefinitions.BindableDerivedListAttributeType}.g.cs", SourceText.From(AttributeDefinitions.BindableDerivedListAttribute, Encoding.UTF8)); @@ -33,44 +30,52 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.BindableDerivedListAttributeType, - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + static (node, _) => node is VariableDeclaratorSyntax + { + Parent.Parent: FieldDeclarationSyntax + { + Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, + AttributeLists.Count: > 0, + }, + }, static (context, token) => GetVariableInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) + .Where(static x => x is not null) + .Select(static (x, _) => x!) .Collect(); // Generate the requested properties context.RegisterSourceOutput(bindableDerivedListInfo, static (context, input) => { - foreach (var diagnostic in input.SelectMany(static x => x.Errors)) - { - // Output the diagnostics - context.ReportDiagnostic(diagnostic.ToDiagnostic()); - } + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + List> groupedPropertyInfo = []; - // Gather all the properties that are valid and group them by the target information. - var groupedPropertyInfo = input - .Where(static x => x.Value != null) - .Select(static x => x.Value!).GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); - - if (groupedPropertyInfo.Length == 0) + foreach (var result in input) { - return; - } - - foreach (var grouping in groupedPropertyInfo) - { - var items = grouping.ToImmutableArray(); + foreach (var diagnostic in result.Errors.AsImmutableArray()) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } - if (items.Length == 0) + if (result.Value is not BindableDerivedListInfo propertyInfo) { continue; } - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + var targetInfo = propertyInfo.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedPropertyInfo.TryGetValue(key, out var properties)) + { + properties = []; + groupedPropertyInfo.Add(key, properties); + } + + properties.Add(propertyInfo); + } + + foreach (var grouping in groupedPropertyInfo) + { + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.Value.ToArray()); context.AddSource($"{grouping.Key.FileHintName}.BindableDerivedList.g.cs", source); } }); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/Models/BindableDerivedListInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/Models/BindableDerivedListInfo.cs index 74a43741..15e49dca 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/Models/BindableDerivedListInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/Models/BindableDerivedListInfo.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; @@ -8,9 +7,15 @@ namespace ReactiveUI.SourceGenerators.BindableDerivedList.Models; -/// -/// A model with gathered info on a given field. -/// +/// A model with gathered info on a given field. +/// The target type that owns the generated property. +/// The property's fully qualified type name. +/// The source field name. +/// The generated property name. +/// Whether the field type permits null. +/// Whether a member-not-null annotation is required. +/// Attributes copied to the generated property. +/// The generated property's accessibility. internal sealed record BindableDerivedListInfo( TargetInfo TargetInfo, string TypeNameWithNullabilityAnnotations, diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs index bffae8e7..bda8a023 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -16,63 +15,61 @@ namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class AttributeDataExtensions { - /// - /// Tries to get a given named argument value from an instance, if present. - /// - /// The type of argument to check. - /// The target instance to check. - /// The name of the argument to check. - /// The resulting argument value, if present. - /// Whether or not contains an argument named with a valid value. - public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out T? value) + /// Provides extension members for attribute data. + /// The attribute data to extend. + extension(AttributeData attributeData) { - if (attributeData is null) + /// Tries to get a given named argument value from this instance, if present. + /// The type of argument to check. + /// The name of the argument to check. + /// The resulting argument value, if present. + /// Whether this instance contains an argument named with a valid value. + internal bool TryGetNamedArgument(string name, out T? value) { - value = default; - return false; - } + if (attributeData is null) + { + value = default; + return false; + } - // NamedArguments returns a default ImmutableArray when attribute data is incomplete/malformed. - // Guard with IsDefaultOrEmpty rather than catching NullReferenceException to avoid masking real bugs. - if (!attributeData.NamedArguments.IsDefaultOrEmpty) - { - foreach (var properties in attributeData.NamedArguments) + // NamedArguments returns a default ImmutableArray when attribute data is incomplete/malformed. + // Guard with IsDefaultOrEmpty rather than catching NullReferenceException to avoid masking real bugs. + if (!attributeData.NamedArguments.IsDefaultOrEmpty) { - if (properties.Key == name) + foreach (var properties in attributeData.NamedArguments) { - return TryConvertNamedArgument(properties.Value, out value); + if (properties.Key == name) + { + return TryConvertNamedArgument(properties.Value, out value); + } } } - } - - value = default; - return false; - } - - /// - /// Gets the named argument. - /// - /// The type of argument to get. - /// The attribute data. - /// The name. - /// The named argument value. - public static T? GetNamedArgument(this AttributeData attributeData, string name) - { - if (attributeData is null) - { - return default; + value = default; + return false; } - // NamedArguments returns a default ImmutableArray when attribute data is incomplete/malformed. - // Guard with IsDefaultOrEmpty rather than catching NullReferenceException to avoid masking real bugs. - if (!attributeData.NamedArguments.IsDefaultOrEmpty) + /// Gets a named argument from this instance. + /// The type of argument to get. + /// The name of the argument. + /// The named argument value. + internal T? GetNamedArgument(string name) { + if (attributeData is null) + { + return default; + } + + // NamedArguments returns a default ImmutableArray when attribute data is incomplete/malformed. + // Guard with IsDefaultOrEmpty rather than catching NullReferenceException to avoid masking real bugs. + if (attributeData.NamedArguments.IsDefaultOrEmpty) + { + return default; + } + foreach (var properties in attributeData.NamedArguments) { if (properties.Key == name) @@ -80,129 +77,123 @@ public static bool TryGetNamedArgument(this AttributeData attributeData, stri return TryConvertNamedArgument(properties.Value, out T? value) ? value : default; } } - } - return default; - } + return default; + } - /// - /// Enumerates all items in a flattened sequence of constructor arguments for a given instance. - /// - /// The type of constructor arguments to retrieve. - /// The target instance to get the arguments from. - /// A sequence of all constructor arguments of the specified type from . - public static IEnumerable GetConstructorArguments(this AttributeData attributeData) - where T : class - { - static IEnumerable Enumerate(IEnumerable constants) + /// Enumerates all items in a flattened sequence of constructor arguments from this instance. + /// The type of constructor arguments to retrieve. + /// A sequence of all constructor arguments of the specified type. + internal IEnumerable GetConstructorArguments() + where T : class { - foreach (var constant in constants) + static IEnumerable Enumerate(IEnumerable constants) { - if (constant.IsNull) + foreach (var constant in constants) { - yield return null; - } + if (constant.IsNull) + { + yield return null; + } - if (constant.Kind == TypedConstantKind.Primitive && - constant.Value is T value) - { - yield return value; - } - else if (constant.Kind == TypedConstantKind.Array) - { - foreach (var item in Enumerate(constant.Values)) + if (constant.Kind == TypedConstantKind.Primitive + && constant.Value is T value) { - yield return item; + yield return value; + } + else if (constant.Kind == TypedConstantKind.Array) + { + foreach (var item in Enumerate(constant.Values)) + { + yield return item; + } } } } - } - return Enumerate(attributeData.ConstructorArguments); - } + return Enumerate(attributeData.ConstructorArguments); + } - /// - /// Gathers the forwarded attributes from class. - /// - /// The attribute data. - /// The semantic model. - /// The class declaration. - /// The token. - /// The class attributes information. - public static void GatherForwardedAttributesFromClass( - this AttributeData attributeData, + /// Gathers forwarded attributes from a class for this instance. + /// The semantic model. + /// The class declaration. + /// The token. + /// The class attributes information. + internal void GatherForwardedAttributesFromClass( SemanticModel semanticModel, ClassDeclarationSyntax classDeclaration, CancellationToken token, out ImmutableArray classAttributesInfo) - { - using var classAttributesInfoBuilder = ImmutableArrayBuilder.Rent(); - - static void GatherForwardedAttributes( - AttributeData attributeData, - SemanticModel semanticModel, - ClassDeclarationSyntax classDeclaration, - CancellationToken token, - ImmutableArrayBuilder classAttributesInfo) { - // Gather explicit forwarded attributes info - foreach (var attributeList in classDeclaration.AttributeLists) + using var classAttributesInfoBuilder = ImmutableArrayBuilder.Rent(); + + static void GatherForwardedAttributes( + AttributeData attributeData, + SemanticModel semanticModel, + ClassDeclarationSyntax classDeclaration, + CancellationToken token, + ImmutableArrayBuilder classAttributesInfo) { - foreach (var attribute in attributeList.Attributes) + // Gather explicit forwarded attributes info + foreach (var attributeList in classDeclaration.AttributeLists) { - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) + foreach (var attribute in attributeList.Attributes) { - continue; + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) + { + continue; + } + + var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) + { + continue; + } + + var ignoreAttribute = attributeData.AttributeClass?.GetFullyQualifiedMetadataName(); + if (attributeInfo.TypeName.Contains(ignoreAttribute)) + { + continue; + } + + // Add the new attribute info to the right builder + classAttributesInfo.Add(attributeInfo); } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - continue; - } - - var ignoreAttribute = attributeData.AttributeClass?.GetFullyQualifiedMetadataName(); - if (attributeInfo.TypeName.Contains(ignoreAttribute)) - { - continue; - } - - // Add the new attribute info to the right builder - classAttributesInfo.Add(attributeInfo); } } - } - // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributes(attributeData, semanticModel, classDeclaration, token, classAttributesInfoBuilder); + // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications + GatherForwardedAttributes(attributeData, semanticModel, classDeclaration, token, classAttributesInfoBuilder); - classAttributesInfo = classAttributesInfoBuilder.ToImmutable(); - } + classAttributesInfo = classAttributesInfoBuilder.ToImmutable(); + } - /// - /// Gets the type of the generic. - /// - /// The attribute data. - /// A String. - public static string? GetGenericType(this AttributeData attributeData) - { - var success = attributeData?.AttributeClass?.ToDisplayString(); - if (string.IsNullOrWhiteSpace(success)) + /// Gets the generic type from this instance. + /// The generic type name, if present. + internal string? GetGenericType() { - return null; - } + var attributeClassName = attributeData.AttributeClass?.ToDisplayString(); + if (string.IsNullOrWhiteSpace(attributeClassName)) + { + return null; + } - var attributeClassName = success ?? string.Empty; - var start = attributeClassName.IndexOf('<'); - var end = attributeClassName.LastIndexOf('>'); + var start = attributeClassName!.IndexOf('<'); + var end = attributeClassName.LastIndexOf('>'); - return start >= 0 && end > start - ? attributeClassName.Substring(start + 1, end - start - 1) - : null; + return start >= 0 && end > start + ? attributeClassName.Substring(start + 1, end - start - 1) + : null; + } } + /// Tries to convert a typed constant to the requested value type. + /// The requested value type. + /// The typed constant to convert. + /// The converted value, if conversion succeeds. + /// Whether conversion succeeds. private static bool TryConvertNamedArgument(in TypedConstant typedConstant, out T? value) { var rawValue = TryGetRawValue(typedConstant); @@ -263,6 +254,9 @@ private static bool TryConvertNamedArgument(in TypedConstant typedConstant, o } } + /// Gets the raw value represented by a typed constant. + /// The typed constant to inspect. + /// The raw value, if available. private static object? TryGetRawValue(in TypedConstant typedConstant) { if (typedConstant.Kind == TypedConstantKind.Error) @@ -270,32 +264,48 @@ private static bool TryConvertNamedArgument(in TypedConstant typedConstant, o return null; } - if (typedConstant.Type?.TypeKind == TypeKind.Enum) + return typedConstant.Type?.TypeKind == TypeKind.Enum + ? GetEnumRawValue(typedConstant) + : typedConstant.Value; + } + + /// Gets the raw value represented by an enum typed constant. + /// The enum typed constant to inspect. + /// The raw enum value, if available. + private static object? GetEnumRawValue(in TypedConstant typedConstant) + { + if (typedConstant.Value is IFieldSymbol fieldSymbol) { - if (typedConstant.Value is IFieldSymbol fieldSymbol) - { - return fieldSymbol.ConstantValue; - } + return fieldSymbol.ConstantValue; + } - if (typedConstant.Value is not null) - { - return typedConstant.Value; - } + return typedConstant.Value + ?? (typedConstant.Type is INamedTypeSymbol enumType + ? GetEnumMemberValue(enumType, typedConstant.ToCSharpString()) + : null); + } - if (typedConstant.Type is INamedTypeSymbol enumType) - { - var csharpValue = typedConstant.ToCSharpString(); + /// Gets the constant value of an enum member rendered in C# source. + /// The enum type containing the member. + /// The rendered enum value. + /// The enum member constant value, if it can be resolved. + private static object? GetEnumMemberValue(INamedTypeSymbol enumType, string csharpValue) + { + if (string.IsNullOrWhiteSpace(csharpValue)) + { + return null; + } - if (!string.IsNullOrWhiteSpace(csharpValue)) - { - var enumMemberName = csharpValue.Split('.').LastOrDefault(); - return enumType.GetMembers(enumMemberName ?? string.Empty).OfType().FirstOrDefault()?.ConstantValue; - } + var separatorIndex = csharpValue.LastIndexOf('.'); + var enumMemberName = csharpValue[(separatorIndex + 1)..]; + foreach (var member in enumType.GetMembers(enumMemberName)) + { + if (member is IFieldSymbol enumMember) + { + return enumMember.ConstantValue; } - - return null; } - return typedConstant.Value; + return null; } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/CompilationExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/CompilationExtensions.cs index dcc29158..85ee805b 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/CompilationExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/CompilationExtensions.cs @@ -1,79 +1,58 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class CompilationExtensions { - /// - /// - /// Checks whether or not a type with a specified metadata name is accessible from a given instance. - /// - /// - /// This method enumerates candidate type symbols to find a match in the following order: - /// - /// - /// If only one type with the given name is found within the compilation and its referenced assemblies, check its accessibility. - /// - /// - /// If the current defines the symbol, check its accessibility. - /// - /// - /// Otherwise, check whether the type exists and is accessible from any of the referenced assemblies. - /// - /// - /// - /// - /// The to consider for analysis. - /// The fully-qualified metadata type name to find. - /// Whether a type with the specified metadata name can be accessed from the given compilation. - public static bool HasAccessibleTypeWithMetadataName(this Compilation compilation, string fullyQualifiedMetadataName) + /// Provides extension members for a compilation. + /// The compilation to extend. + extension(Compilation compilation) { - // Try to get the unique type with this name - var type = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); - - // If there is only a single matching symbol, check its accessibility - if (type is not null) + /// Checks whether a type with a specified metadata name is accessible from this compilation. + /// The fully-qualified metadata type name to find. + /// Whether a type with the specified metadata name can be accessed from this compilation. + internal bool HasAccessibleTypeWithMetadataName(string fullyQualifiedMetadataName) { - return type.CanBeAccessedFrom(compilation.Assembly); - } + var type = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); - // Otherwise, try to get the unique type with this name originally defined in 'compilation' - type ??= compilation.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName); + if (type is not null) + { + return type.CanBeAccessedFrom(compilation.Assembly); + } - if (type is not null) - { - return type.CanBeAccessedFrom(compilation.Assembly); - } + type = compilation.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName); - // Otherwise, check whether the type is defined and accessible from any of the referenced assemblies - foreach (var module in compilation.Assembly.Modules) - { - foreach (var referencedAssembly in module.ReferencedAssemblySymbols) + if (type is not null) { - if (referencedAssembly.GetTypeByMetadataName(fullyQualifiedMetadataName) is not INamedTypeSymbol currentType) - { - continue; - } + return type.CanBeAccessedFrom(compilation.Assembly); + } - switch (currentType.GetEffectiveAccessibility()) + foreach (var module in compilation.Assembly.Modules) + { + foreach (var referencedAssembly in module.ReferencedAssemblySymbols) { - case Accessibility.Public: - case Accessibility.Internal when referencedAssembly.GivesAccessTo(compilation.Assembly): - return true; - default: + if (referencedAssembly.GetTypeByMetadataName(fullyQualifiedMetadataName) is not INamedTypeSymbol currentType) + { continue; + } + + switch (currentType.GetEffectiveAccessibility()) + { + case Accessibility.Public: + case Accessibility.Internal when referencedAssembly.GivesAccessTo(compilation.Assembly): + return true; + default: + continue; + } } } - } - return false; + return false; + } } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ContextExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ContextExtensions.cs index 26613fd2..e7ba976f 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ContextExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ContextExtensions.cs @@ -1,10 +1,8 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; -using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -15,107 +13,306 @@ namespace ReactiveUI.SourceGenerators.Extensions; +/// Provides extension members used while processing generator contexts. internal static class ContextExtensions { - internal static void GetForwardedAttributes( - this in GeneratorAttributeSyntaxContext context, - ImmutableArrayBuilder builder, - ISymbol symbol, - in SyntaxList attributeListSyntaxes, - CancellationToken token, - out ImmutableArray forwardedAttributes) + /// The first ReactiveUI version that supports the current generator behavior. + private const int CurrentGeneratorBehaviorMinimumMajorVersion = 23; + + /// The metadata name of the ReactiveUI primitive void type. + private const string RxVoidMetadataName = "ReactiveUI.Primitives.RxVoid"; + + /// Provides extension members for compilations. + /// The compilation to extend. + extension(Compilation compilation) { - using var forwardedAttributeBuilder = ImmutableArrayBuilder.Rent(); + /// Gets the ReactiveUI integration supported by this compilation. + /// The ReactiveUI API and command behavior supported by this compilation. + internal ReactiveUiIntegration GetReactiveUiIntegration() + { + if (compilation.GetTypeByMetadataName("ReactiveUI.Reactive.ReactiveCommand") is not null) + { + return new(ReactiveUiApi.SystemReactive, true); + } - var symbolAttributes = symbol.GetAttributes(); + var legacyCommand = compilation.GetTypeByMetadataName("ReactiveUI.ReactiveCommand"); + if (legacyCommand is not null) + { + var majorVersion = legacyCommand.ContainingAssembly.Identity.Version.Major; + return HasPrimitiveRxVoid(legacyCommand) + ? new(ReactiveUiApi.Primitives, true) + : new(ReactiveUiApi.Legacy, majorVersion >= CurrentGeneratorBehaviorMinimumMajorVersion); + } + + return GetReferencedReactiveUiIntegration(compilation); + } + } - // if attributes contains the [Reactive] attribute, we should not forward any attributes without field targets - var isReactiveFromPartialProperty = symbol is IPropertySymbol && symbolAttributes.Any(a => a.AttributeClass?.HasFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType) == true); - if (!isReactiveFromPartialProperty && symbolAttributes.Length > 1) + /// Provides extension members for generator attribute syntax contexts. + /// The generator attribute syntax context to extend. + extension(in GeneratorAttributeSyntaxContext context) + { + /// Gets attributes that should be forwarded from the source member. + /// The builder to receive diagnostics. + /// The source symbol. + /// The attribute lists declared on the source member. + /// The cancellation token. + /// The generated attribute source text. + internal void GetForwardedAttributes( + ImmutableArrayBuilder builder, + ISymbol symbol, + in SyntaxList attributeListSyntaxes, + CancellationToken token, + out ImmutableArray forwardedAttributes) { - // Gather attributes info - foreach (var attribute in symbolAttributes) + using var attributes = ImmutableArrayBuilder.Rent(); + AddAutomaticForwardedAttributes(symbol, attributes, token); + AddExplicitForwardedAttributes(context, builder, symbol, attributeListSyntaxes, attributes, token); + forwardedAttributes = ConvertToSourceAttributes(attributes.ToImmutable()); + } + } + + /// Provides extension members for incremental generator initialization contexts. + /// The initialization context to extend. + extension(in IncrementalGeneratorInitializationContext context) + { + /// Gets a provider for the ReactiveUI API integration used by the compilation. + /// A provider that produces the ReactiveUI integration details. + internal IncrementalValueProvider ReactiveUiIntegration() => + context.CompilationProvider.Select(static (compilation, token) => { token.ThrowIfCancellationRequested(); + return compilation.GetReactiveUiIntegration(); + }); + } - // Track the current attribute for forwarding if it is a validation attribute - if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) - { - forwardedAttributeBuilder.Add(AttributeInfo.Create(attribute)); - } + /// Adds attributes automatically forwarded from a source symbol. + /// The source symbol. + /// The destination attributes. + /// The cancellation token. + private static void AddAutomaticForwardedAttributes( + ISymbol symbol, + ImmutableArrayBuilder attributes, + CancellationToken token) + { + var symbolAttributes = symbol.GetAttributes(); + if (IsReactivePartialProperty(symbol, symbolAttributes) || symbolAttributes.Length <= 1) + { + return; + } - // Track the current attribute for forwarding if it is a Json Serialization attribute - if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.Text.Json.Serialization.JsonAttribute") == true) - { - forwardedAttributeBuilder.Add(AttributeInfo.Create(attribute)); - } + foreach (var attribute in symbolAttributes) + { + token.ThrowIfCancellationRequested(); + if (ShouldForwardAttribute(attribute)) + { + attributes.Add(AttributeInfo.Create(attribute)); + } + } + } - // Also track the current attribute for forwarding if it is of any of the following types: - if (attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || - attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.DataMemberAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.IgnoreDataMemberAttribute") == true) - { - forwardedAttributeBuilder.Add(AttributeInfo.Create(attribute)); - } + /// Determines whether a symbol is a partial property decorated with ReactiveAttribute. + /// The symbol to inspect. + /// The attributes declared on the symbol. + /// Whether the symbol is a reactive partial property. + private static bool IsReactivePartialProperty(ISymbol symbol, ImmutableArray attributes) + { + if (symbol is not IPropertySymbol) + { + return false; + } + + foreach (var attribute in attributes) + { + if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType) == true) + { + return true; } } - token.ThrowIfCancellationRequested(); + return false; + } + + /// Determines whether an attribute should be forwarded automatically. + /// The attribute to inspect. + /// Whether the attribute should be forwarded. + private static bool ShouldForwardAttribute(AttributeData attribute) + { + var attributeClass = attribute.AttributeClass; + return IsValidationAttribute(attributeClass) + || IsJsonAttribute(attributeClass) + || IsDataAnnotationAttribute(attributeClass) + || IsSerializationAttribute(attributeClass); + } + + /// Determines whether a type is a validation attribute. + /// The attribute type to inspect. + /// Whether the type is a validation attribute. + private static bool IsValidationAttribute(INamedTypeSymbol? attributeClass) => + attributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true; + + /// Determines whether a type is a JSON serialization attribute. + /// The attribute type to inspect. + /// Whether the type is a JSON serialization attribute. + private static bool IsJsonAttribute(INamedTypeSymbol? attributeClass) => + attributeClass?.InheritsFromFullyQualifiedMetadataName("System.Text.Json.Serialization.JsonAttribute") == true; + + /// Determines whether a type is a supported data annotation attribute. + /// The attribute type to inspect. + /// Whether the type is a supported data annotation attribute. + private static bool IsDataAnnotationAttribute(INamedTypeSymbol? attributeClass) => + HasOrInheritsFrom(attributeClass, "System.ComponentModel.DataAnnotations.UIHintAttribute") + || HasOrInheritsFrom(attributeClass, "System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") + || HasMetadataName(attributeClass, "System.ComponentModel.DataAnnotations.DisplayAttribute") + || HasMetadataName(attributeClass, "System.ComponentModel.DataAnnotations.EditableAttribute") + || HasMetadataName(attributeClass, "System.ComponentModel.DataAnnotations.KeyAttribute"); + + /// Determines whether a type is a supported serialization attribute. + /// The attribute type to inspect. + /// Whether the type is a supported serialization attribute. + private static bool IsSerializationAttribute(INamedTypeSymbol? attributeClass) => + HasMetadataName(attributeClass, "System.Runtime.Serialization.DataMemberAttribute") + || HasMetadataName(attributeClass, "System.Runtime.Serialization.IgnoreDataMemberAttribute"); + + /// Determines whether an attribute type has or inherits from a metadata name. + /// The attribute type to inspect. + /// The metadata name to match. + /// Whether the type has or inherits from the metadata name. + private static bool HasOrInheritsFrom(INamedTypeSymbol? attributeClass, string metadataName) => + attributeClass?.HasOrInheritsFromFullyQualifiedMetadataName(metadataName) == true; + + /// Determines whether an attribute type has a metadata name. + /// The attribute type to inspect. + /// The metadata name to match. + /// Whether the type has the metadata name. + private static bool HasMetadataName(INamedTypeSymbol? attributeClass, string metadataName) => + attributeClass?.HasFullyQualifiedMetadataName(metadataName) == true; - // Gather explicit forwarded attributes info - foreach (var attributeList in attributeListSyntaxes) + /// Adds attributes explicitly targeted at the generated property or field. + /// The generator context. + /// The builder to receive diagnostics. + /// The source symbol. + /// The attribute lists to inspect. + /// The destination attributes. + /// The cancellation token. + private static void AddExplicitForwardedAttributes( + in GeneratorAttributeSyntaxContext context, + ImmutableArrayBuilder diagnostics, + ISymbol symbol, + in SyntaxList attributeLists, + ImmutableArrayBuilder attributes, + CancellationToken token) + { + foreach (var attributeList in attributeLists) { - // Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a - // CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor - // that recognizes uses of this target specifically to support [ObservableAsProperty]. - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword) && attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.FieldKeyword)) + if (!IsPropertyOrFieldTarget(attributeList)) { continue; } token.ThrowIfCancellationRequested(); - foreach (var attribute in attributeList.Attributes) { - if (!context.SemanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - builder.Add( - InvalidPropertyTargetedAttributeOnObservableAsPropertyField, - attribute, - symbol, - attribute.Name); - continue; - } + AddExplicitForwardedAttribute(context, diagnostics, symbol, attribute, attributes, token); + } + } + } - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + /// Determines whether an attribute list targets the generated property or field. + /// The attribute list to inspect. + /// Whether the list targets a generated member. + private static bool IsPropertyOrFieldTarget(AttributeListSyntax attributeList) => + attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.PropertyKeyword) + or SyntaxToken(SyntaxKind.FieldKeyword); - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, context.SemanticModel, attributeArguments, token, out var attributeInfo)) + /// Adds one explicitly forwarded attribute or reports the appropriate diagnostic. + /// The generator context. + /// The builder to receive diagnostics. + /// The source symbol. + /// The attribute syntax to inspect. + /// The destination attributes. + /// The cancellation token. + private static void AddExplicitForwardedAttribute( + in GeneratorAttributeSyntaxContext context, + ImmutableArrayBuilder diagnostics, + ISymbol symbol, + AttributeSyntax attribute, + ImmutableArrayBuilder attributes, + CancellationToken token) + { + if (!context.SemanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeType)) + { + diagnostics.Add(InvalidPropertyTargetedAttributeOnObservableAsPropertyField, attribute, symbol, attribute.Name); + return; + } + + if (!AttributeInfo.TryCreate( + attributeType, + context.SemanticModel, + attribute.ArgumentList?.Arguments ?? [], + token, + out var attributeInfo)) + { + diagnostics.Add(InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField, attribute, symbol, attribute.Name); + return; + } + + attributes.Add(attributeInfo); + } + + /// Converts forwarded attribute metadata to generated source text. + /// The attributes to convert. + /// The generated source text. + private static ImmutableArray ConvertToSourceAttributes(ImmutableArray attributes) + { + using var sourceAttributes = ImmutableArrayBuilder.Rent(); + foreach (var attribute in attributes) + { + sourceAttributes.Add(attribute.ToString()); + } + + return sourceAttributes.ToImmutable(); + } + + /// Determines whether a ReactiveUI command exposes the primitive void type. + /// The command type to inspect. + /// Whether a create method exposes ReactiveUI.Primitives.RxVoid. + private static bool HasPrimitiveRxVoid(INamedTypeSymbol command) + { + foreach (var member in command.GetMembers("Create")) + { + if (member is not IMethodSymbol method || method.ReturnType is not INamedTypeSymbol returnType) + { + continue; + } + + foreach (var typeArgument in returnType.TypeArguments) + { + if (typeArgument.ToDisplayString() == RxVoidMetadataName) { - builder.Add( - InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField, - attribute, - symbol, - attribute.Name); - continue; + return true; } - - forwardedAttributeBuilder.Add(attributeInfo); } } - var attributes = forwardedAttributeBuilder.ToImmutable(); - forwardedAttributes = attributes.Select(static a => a.ToString()).ToImmutableArray(); + return false; } - internal static IncrementalValueProvider ReactiveUIVersionIsGreaterThan22(this in IncrementalGeneratorInitializationContext context) => - context.CompilationProvider.Select(static (compilation, token) => + /// Gets the integration details from a referenced ReactiveUI assembly. + /// The compilation to inspect. + /// The detected ReactiveUI integration. + private static ReactiveUiIntegration GetReferencedReactiveUiIntegration(Compilation compilation) + { + foreach (var assembly in compilation.ReferencedAssemblyNames) { - token.ThrowIfCancellationRequested(); - return compilation.ReferencedAssemblyNames.FirstOrDefault(a => a.Name == AttributeDefinitions.ReactiveUI)?.Version.Major > 22; - }); + if (assembly.Name == AttributeDefinitions.ReactiveUI) + { + return new( + ReactiveUiApi.Legacy, + assembly.Version.Major >= CurrentGeneratorBehaviorMinimumMajorVersion); + } + } + + return default; + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/DiagnosticsExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/DiagnosticsExtensions.cs index 1e79bb3f..ed7d9404 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/DiagnosticsExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/DiagnosticsExtensions.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; @@ -9,34 +8,23 @@ namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for , specifically for reporting diagnostics. -/// +/// Extension methods for , specifically for reporting diagnostics. internal static class DiagnosticsExtensions { - /// - /// Adds a new diagnostics to the target builder. - /// - /// The collection of produced instances. - /// The input for the diagnostics to create. - /// The source to attach the diagnostics to. - /// The optional arguments for the formatted message to include. - public static void Add( - this ImmutableArrayBuilder diagnostics, - DiagnosticDescriptor descriptor, - ISymbol symbol, - params object[] args) => diagnostics.Add(DiagnosticInfo.Create(descriptor, symbol, args)); + /// Provides extension members for a diagnostic builder. + /// The diagnostic builder to extend. + extension(ImmutableArrayBuilder diagnostics) + { + /// Adds a diagnostic associated with a symbol to this builder. + /// The input for the diagnostic to create. + /// The source to attach the diagnostic to. + /// The optional arguments for the formatted message to include. + internal void Add(DiagnosticDescriptor descriptor, ISymbol symbol, params object[] args) => diagnostics.Add(DiagnosticInfo.Create(descriptor, symbol, args)); - /// - /// Adds a new diagnostics to the target builder. - /// - /// The collection of produced instances. - /// The input for the diagnostics to create. - /// The source to attach the diagnostics to. - /// The optional arguments for the formatted message to include. - public static void Add( - this ImmutableArrayBuilder diagnostics, - DiagnosticDescriptor descriptor, - SyntaxNode node, - params object[] args) => diagnostics.Add(DiagnosticInfo.Create(descriptor, node, args)); + /// Adds a diagnostic associated with a syntax node to this builder. + /// The input for the diagnostic to create. + /// The source to attach the diagnostic to. + /// The optional arguments for the formatted message to include. + internal void Add(DiagnosticDescriptor descriptor, SyntaxNode node, params object[] args) => diagnostics.Add(DiagnosticInfo.Create(descriptor, node, args)); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs index d995ea7e..51aba332 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs @@ -1,162 +1,126 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Globalization; -using System.Linq; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Extensions; +/// Extension methods for ReactiveUI field, property, and method symbols. internal static class FieldSyntaxExtensions { - /// - /// Get the generated property name for an input field. - /// - /// The input instance to process. - /// The generated property name for . - internal static string GetGeneratedPropertyName(this IFieldSymbol fieldSymbol) - { - var propertyName = fieldSymbol.Name; + /// The number of characters in a member-style backing-field prefix. + private const int FieldPrefixLength = 2; - if (propertyName.StartsWith("m_")) - { - propertyName = propertyName.Substring(2); - } - else if (propertyName.StartsWith("_")) - { - propertyName = propertyName.TrimStart('_'); - } + /// The metadata name of ReactiveUI's observable object base type. + private const string ReactiveObjectTypeName = "ReactiveUI.ReactiveObject"; - return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; - } + /// The metadata name of ReactiveUI's observable object interface. + private const string ReactiveObjectInterfaceTypeName = "ReactiveUI.IReactiveObject"; - internal static string GetGeneratedFieldName(this IPropertySymbol propertySymbol) + /// Provides operations for annotated field symbols. + /// The field symbol receiving the extension operation. + extension(IFieldSymbol fieldSymbol) { - var propertyName = propertySymbol.Name; + /// Gets the generated property name for an input field. + /// The generated property name. + internal string GetGeneratedPropertyName() + { + var propertyName = fieldSymbol.Name; + + if (propertyName.StartsWith("m_", System.StringComparison.Ordinal)) + { + propertyName = propertyName[FieldPrefixLength..]; + } + else if (propertyName.StartsWith("_", System.StringComparison.Ordinal)) + { + propertyName = propertyName.TrimStart('_'); + } + + return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName[1..]}"; + } - return $"_{char.ToLower(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; + /// Gets nullability information for a generated property. + /// The semantic model for the current run. + /// Whether the property type supports nullability. + /// Whether MemberNotNullAttribute should be used on the setter. + internal void GetNullabilityInfo( + SemanticModel semanticModel, + out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool includeMemberNotNullOnSetAccessor) => + GetNullabilityInfo(fieldSymbol.Type, semanticModel, out isReferenceTypeOrUnconstraindTypeParameter, out includeMemberNotNullOnSetAccessor); + + /// Validates the containing type for a given field being annotated. + /// Whether the containing type is valid. + internal bool IsTargetTypeValid() => IsTargetTypeValid(fieldSymbol.ContainingType); } - /// - /// Gets the nullability info on the generated property. - /// - /// The input instance to process. - /// The instance for the current run. - /// Whether the property type supports nullability. - /// Whether MemberNotNullAttribute should be used on the setter. - internal static void GetNullabilityInfo( - this IFieldSymbol fieldSymbol, - SemanticModel semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, - out bool includeMemberNotNullOnSetAccessor) + /// Provides operations for annotated method symbols. + /// The method symbol receiving the extension operation. + extension(IMethodSymbol methodSymbol) { - // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. - // This will cover both reference types as well T when the constraints are not struct or unmanaged. - // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; - - // This is used to avoid nullability warnings when setting the property from a constructor, in case the field - // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. - // Consider this example: - // - // partial class MyViewModel : ReactiveObject - // { - // public MyViewModel() - // { - // Name = "Bob"; - // } - // - // [Reactive] - // private string _name; - // } - // - // The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name - // is set, the compiler can determine that the name backing field is also being set (to a non null value). - // Of course, this can only be the case if the field type is also of a type that could be in a null state. - includeMemberNotNullOnSetAccessor = - isReferenceTypeOrUnconstraindTypeParameter && - fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && - semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); + /// Validates the containing type for a given method being annotated. + /// Whether the containing type is valid. + internal bool IsTargetTypeValid() => IsTargetTypeValid(methodSymbol.ContainingType); } - internal static void GetNullabilityInfo( - this IPropertySymbol propertySymbol, - SemanticModel semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, - out bool includeMemberNotNullOnSetAccessor) + /// Provides operations for annotated property symbols. + /// The property symbol receiving the extension operation. + extension(IPropertySymbol propertySymbol) { - // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. - // This will cover both reference types as well T when the constraints are not struct or unmanaged. - // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !propertySymbol.Type.IsValueType; - - // This is used to avoid nullability warnings when setting the property from a constructor, in case the field - // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. - // Consider this example: - // - // partial class MyViewModel : ReactiveObject - // { - // public MyViewModel() - // { - // Name = "Bob"; - // } - // - // [Reactive] - // private string _name; - // } - // - // The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name - // is set, the compiler can determine that the name backing field is also being set (to a non null value). - // Of course, this can only be the case if the field type is also of a type that could be in a null state. - includeMemberNotNullOnSetAccessor = - isReferenceTypeOrUnconstraindTypeParameter && - propertySymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && - semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); - } + /// Gets the generated backing-field name for an input property. + /// The generated backing-field name. + internal string GetGeneratedFieldName() + { + var propertyName = propertySymbol.Name; - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - internal static bool IsTargetTypeValid(this IFieldSymbol fieldSymbol) - { - var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = fieldSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType); + return $"_{char.ToLower(propertyName[0], CultureInfo.InvariantCulture)}{propertyName[1..]}"; + } - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + /// Gets nullability information for a generated property. + /// The semantic model for the current run. + /// Whether the property type supports nullability. + /// Whether MemberNotNullAttribute should be used on the setter. + internal void GetNullabilityInfo( + SemanticModel semanticModel, + out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool includeMemberNotNullOnSetAccessor) => + GetNullabilityInfo(propertySymbol.Type, semanticModel, out isReferenceTypeOrUnconstraindTypeParameter, out includeMemberNotNullOnSetAccessor); + + /// Validates the containing type for a given property being annotated. + /// Whether the containing type is valid. + internal bool IsTargetTypeValid() => IsTargetTypeValid(propertySymbol.ContainingType); } - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - internal static bool IsTargetTypeValid(this IPropertySymbol propertySymbol) + /// Determines whether a containing type supports ReactiveUI-generated members. + /// The type that owns the annotated member. + /// Whether the containing type is a supported ReactiveUI observable type. + private static bool IsTargetTypeValid(INamedTypeSymbol containingType) { - var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = propertySymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType); + var isObservableObject = containingType.InheritsFromFullyQualifiedMetadataName(ReactiveObjectTypeName); + var isIObservableObject = containingType.ImplementsFullyQualifiedMetadataName(ReactiveObjectInterfaceTypeName); + var hasObservableObjectAttribute = containingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType); return isIObservableObject || isObservableObject || hasObservableObjectAttribute; } - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - internal static bool IsTargetTypeValid(this IMethodSymbol methodSymbol) + /// Gets nullability information for a property generated from a type. + /// The member type to evaluate. + /// The semantic model for the current run. + /// Whether the property type supports nullability. + /// Whether MemberNotNullAttribute should be used on the setter. + private static void GetNullabilityInfo( + ITypeSymbol typeSymbol, + SemanticModel semanticModel, + out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool includeMemberNotNullOnSetAccessor) { - var isObservableObject = methodSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = methodSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = methodSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType); - - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + isReferenceTypeOrUnconstraindTypeParameter = !typeSymbol.IsValueType; + includeMemberNotNullOnSetAccessor = + isReferenceTypeOrUnconstraindTypeParameter + && typeSymbol.NullableAnnotation != NullableAnnotation.Annotated + && semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/INamedTypeSymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/INamedTypeSymbolExtensions.cs index 4005654f..257257b0 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/INamedTypeSymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/INamedTypeSymbolExtensions.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,18 +8,17 @@ namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class INamedTypeSymbolExtensions { - /// - /// Gets all member symbols from a given instance, including inherited ones. - /// - /// The input instance. - /// A sequence of all member symbols for . - public static IEnumerable GetAllMembers(this INamedTypeSymbol symbol) + /// Provides extension members for a named type symbol. + /// The named type symbol to extend. + extension(INamedTypeSymbol symbol) { + /// Gets all member symbols from this instance, including inherited ones. + /// A sequence of all member symbols. + internal IEnumerable GetAllMembers() + { for (var currentSymbol = symbol; currentSymbol is { SpecialType: not SpecialType.System_Object }; currentSymbol = currentSymbol.BaseType) { foreach (var memberSymbol in currentSymbol.GetMembers()) @@ -28,16 +26,13 @@ public static IEnumerable GetAllMembers(this INamedTypeSymbol symbol) yield return memberSymbol; } } - } + } - /// - /// Gets all member symbols from a given instance, including inherited ones. - /// - /// The input instance. - /// The name of the members to look for. - /// A sequence of all member symbols for . - public static IEnumerable GetAllMembers(this INamedTypeSymbol symbol, string name) - { + /// Gets all member symbols with a given name from this instance, including inherited ones. + /// The name of the members to look for. + /// A sequence of all matching member symbols. + internal IEnumerable GetAllMembers(string name) + { for (var currentSymbol = symbol; currentSymbol is { SpecialType: not SpecialType.System_Object }; currentSymbol = currentSymbol.BaseType) { foreach (var memberSymbol in currentSymbol.GetMembers(name)) @@ -45,30 +40,28 @@ public static IEnumerable GetAllMembers(this INamedTypeSymbol symbol, s yield return memberSymbol; } } - } + } - /// - /// Returns a string representation of the type, such as "class", "struct", or "interface". - /// - /// The type symbol to analyze. - /// A string representing the type kind. - public static string GetTypeString(this INamedTypeSymbol namedTypeSymbol) - { - if (namedTypeSymbol.TypeKind == TypeKind.Interface) + /// Returns a string representation of this type, such as "class", "struct", or "interface". + /// A string representing the type kind. + internal string GetTypeString() + { + if (symbol.TypeKind == TypeKind.Interface) { return "interface"; } - if (namedTypeSymbol.TypeKind == TypeKind.Struct) + if (symbol.TypeKind == TypeKind.Struct) { - return namedTypeSymbol.IsRecord ? "record struct" : "struct"; + return symbol.IsRecord ? "record struct" : "struct"; } - if (namedTypeSymbol.TypeKind == TypeKind.Class) + if (symbol.TypeKind == TypeKind.Class) { - return namedTypeSymbol.IsRecord ? "record" : "class"; + return symbol.IsRecord ? "record" : "class"; } throw new InvalidOperationException("Unknown type kind."); + } } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ISymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ISymbolExtensions.cs index 9170bbe6..2499d4fa 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ISymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ISymbolExtensions.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,169 +8,142 @@ namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class ISymbolExtensions { - /// - /// Gets the fully qualified name for a given symbol. - /// - /// The input instance. - /// The fully qualified name for . - public static string GetFullyQualifiedName(this ISymbol symbol) => - symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - /// - /// Gets the fully qualified name for a given symbol, including nullability annotations. - /// - /// The input instance. - /// The fully qualified name for . - public static string GetFullyQualifiedNameWithNullabilityAnnotations(this ISymbol symbol) => - symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); - - /// - /// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name. - /// - /// The input instance to check. - /// The attribute name to look for. - /// Whether or not has an attribute with the specified name. - public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name) + /// Provides extension members for symbols. + /// The symbol to extend. + extension(ISymbol symbol) { - foreach (var attribute in symbol.GetAttributes()) + /// Gets the fully qualified name for this symbol. + /// The fully qualified name for this symbol. + internal string GetFullyQualifiedName() => + symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + /// Gets the fully qualified name for this symbol, including nullability annotations. + /// The fully qualified name for this symbol. + internal string GetFullyQualifiedNameWithNullabilityAnnotations() => + symbol.ToDisplayString( + SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); + + /// Checks whether this symbol has an attribute with the specified fully qualified metadata name. + /// The attribute name to look for. + /// Whether this symbol has an attribute with the specified name. + internal bool HasAttributeWithFullyQualifiedMetadataName(string name) { - if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true) + foreach (var attribute in symbol.GetAttributes()) { - return true; + if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true) + { + return true; + } } - } - return false; - } + return false; + } - /// - /// Checks whether or not a given symbol has an attribute with the specified type. - /// - /// The input instance to check. - /// The instance for the attribute type to look for. - /// Whether or not has an attribute with the specified type. - public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol) => - TryGetAttributeWithType(symbol, typeSymbol, out _); - - /// - /// Tries to get an attribute with the specified type. - /// - /// The input instance to check. - /// The instance for the attribute type to look for. - /// The resulting attribute, if it was found. - /// Whether or not has an attribute with the specified type. - public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData) - { - foreach (var attribute in symbol.GetAttributes()) + /// Checks whether this symbol has an attribute with the specified type. + /// The attribute type to look for. + /// Whether this symbol has an attribute with the specified type. + internal bool HasAttributeWithType(ITypeSymbol? typeSymbol) => + typeSymbol is not null && symbol.TryGetAttributeWithType(typeSymbol, out _); + + /// Tries to get an attribute with the specified type. + /// The attribute type to look for. + /// The resulting attribute, if it was found. + /// Whether this symbol has an attribute with the specified type. + internal bool TryGetAttributeWithType( + ITypeSymbol typeSymbol, + [NotNullWhen(true)] out AttributeData? attributeData) { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) + foreach (var attribute in symbol.GetAttributes()) { - attributeData = attribute; - - return true; + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) + { + attributeData = attribute; + return true; + } } - } - - attributeData = null; - return false; - } + attributeData = null; + return false; + } - /// - /// Tries to get an attribute with the specified fully qualified metadata name. - /// - /// The input instance to check. - /// The attribute name to look for. - /// The resulting attribute, if it was found. - /// Whether or not has an attribute with the specified name. - public static bool TryGetAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name, [NotNullWhen(true)] out AttributeData? attributeData) - { - foreach (var attribute in symbol.GetAttributes()) + /// Tries to get an attribute with the specified fully qualified metadata name. + /// The attribute name to look for. + /// The resulting attribute, if it was found. + /// Whether this symbol has an attribute with the specified name. + internal bool TryGetAttributeWithFullyQualifiedMetadataName( + string name, + [NotNullWhen(true)] out AttributeData? attributeData) { - if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true) + foreach (var attribute in symbol.GetAttributes()) { - attributeData = attribute; - - return true; + if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true) + { + attributeData = attribute; + return true; + } } - } - - attributeData = null; - - return false; - } - - /// - /// Calculates the effective accessibility for a given symbol. - /// - /// The instance to check. - /// The effective accessibility for . - public static Accessibility GetEffectiveAccessibility(this ISymbol symbol) - { - // Start by assuming it's visible - var visibility = Accessibility.Public; - // Handle special cases - switch (symbol.Kind) - { - case SymbolKind.Alias: return Accessibility.Private; - case SymbolKind.Parameter: return GetEffectiveAccessibility(symbol.ContainingSymbol); - case SymbolKind.TypeParameter: return Accessibility.Private; + attributeData = null; + return false; } - // Traverse the symbol hierarchy to determine the effective accessibility - while (symbol is not null && symbol.Kind != SymbolKind.Namespace) + /// Calculates the effective accessibility for this symbol. + /// The effective accessibility for this symbol. + internal Accessibility GetEffectiveAccessibility() { - switch (symbol.DeclaredAccessibility) + if (symbol.Kind is SymbolKind.Alias or SymbolKind.TypeParameter) + { + return Accessibility.Private; + } + + if (symbol.Kind == SymbolKind.Parameter) { - case Accessibility.NotApplicable: - case Accessibility.Private: + return symbol.ContainingSymbol.GetEffectiveAccessibility(); + } + + var visibility = Accessibility.Public; + for (var current = symbol; current is not null && current.Kind != SymbolKind.Namespace; current = current.ContainingSymbol) + { + var declaredAccessibility = current.DeclaredAccessibility; + if (declaredAccessibility is Accessibility.NotApplicable or Accessibility.Private) + { return Accessibility.Private; - case Accessibility.Internal: - case Accessibility.ProtectedAndInternal: + } + + if (declaredAccessibility is Accessibility.Internal or Accessibility.ProtectedAndInternal) + { visibility = Accessibility.Internal; - break; + } } - symbol = symbol.ContainingSymbol; + return visibility; } - return visibility; - } - - /// - /// Checks whether or not a given symbol can be accessed from a specified assembly. - /// - /// The input instance to check. - /// The assembly to check the accessibility of for. - /// Whether can access . - public static bool CanBeAccessedFrom(this ISymbol symbol, IAssemblySymbol assembly) - { - var accessibility = symbol.GetEffectiveAccessibility(); + /// Checks whether this symbol can be accessed from a specified assembly. + /// The assembly to check access for. + /// Whether can access this symbol. + internal bool CanBeAccessedFrom(IAssemblySymbol assembly) + { + var accessibility = symbol.GetEffectiveAccessibility(); + return accessibility == Accessibility.Public + || (accessibility == Accessibility.Internal && symbol.ContainingAssembly.GivesAccessTo(assembly)); + } - return - accessibility == Accessibility.Public || - (accessibility == Accessibility.Internal && symbol.ContainingAssembly.GivesAccessTo(assembly)); + /// Gets the string representation of the accessibility level of this symbol. + /// The accessibility string for this symbol. + internal string GetAccessibilityString() => symbol.DeclaredAccessibility switch + { + Accessibility.Public => "public", + Accessibility.Private => "private", + Accessibility.Internal => "internal", + Accessibility.Protected => "protected", + Accessibility.ProtectedAndInternal => "protected internal", + Accessibility.ProtectedOrInternal => "private protected", + _ => throw new InvalidOperationException("unknown accessibility") + }; } - - /// - /// Gets the string representation of the accessibility level of the given symbol. - /// - /// The symbol to analyze. - /// A string representing the accessibility level, such as "public" or "private". - public static string GetAccessibilityString(this ISymbol symbol) => symbol.DeclaredAccessibility switch - { - Accessibility.Public => "public", - Accessibility.Private => "private", - Accessibility.Internal => "internal", - Accessibility.Protected => "protected", - Accessibility.ProtectedAndInternal => "protected internal", - Accessibility.ProtectedOrInternal => "private protected", - _ => throw new InvalidOperationException("unknown accessibility") - }; } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs index 1caba69c..1fe0c944 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs @@ -1,26 +1,25 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using Microsoft.CodeAnalysis; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class ITypeSymbolExtensions { - /// - /// Checks whether or not a given has or inherits from a specified type. - /// - /// The target instance to check. + /// Provides metadata-name and hierarchy operations for a type symbol. + /// The type symbol receiving the extension operation. + extension(ITypeSymbol typeSymbol) + { + /// Checks whether or not a given has or inherits from a specified type. /// The full name of the type to check for inheritance. - /// Whether or not is or inherits from . - public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + /// Whether the type symbol is or inherits from . + internal bool HasOrInheritsFromFullyQualifiedMetadataName(string name) { for (var currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) { @@ -33,13 +32,10 @@ public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol return false; } - /// - /// Checks whether or not a given inherits from a specified type. - /// - /// The target instance to check. + /// Checks whether or not a given inherits from a specified type. /// The full name of the type to check for inheritance. - /// Whether or not inherits from . - public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + /// Whether the type symbol inherits from . + internal bool InheritsFromFullyQualifiedMetadataName(string name) { var baseType = typeSymbol.BaseType; @@ -56,13 +52,10 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS return false; } - /// - /// Checks whether or not a given implements a specified interface. - /// - /// The target instance to check. + /// Checks whether or not a given implements a specified interface. /// The full name of the interface to check for inheritance. - /// Whether or not implements . - public static bool ImplementsFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + /// Whether the type symbol implements . + internal bool ImplementsFullyQualifiedMetadataName(string name) { foreach (var implementedInterface in typeSymbol.AllInterfaces) { @@ -75,13 +68,10 @@ public static bool ImplementsFullyQualifiedMetadataName(this ITypeSymbol typeSym return false; } - /// - /// Checks whether or not a given has or inherits from a specified type. - /// - /// The target instance to check. + /// Checks whether or not a given has or inherits from a specified type. /// The full name of the type to check for inheritance. - /// Whether or not is or inherits from . - public static bool HasOrInheritsFromFullyQualifiedMetadataNameStartingWith(this ITypeSymbol typeSymbol, string name) + /// Whether the type symbol is or inherits from . + internal bool HasOrInheritsFromFullyQualifiedMetadataNameStartingWith(string name) { for (var currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) { @@ -94,13 +84,10 @@ public static bool HasOrInheritsFromFullyQualifiedMetadataNameStartingWith(this return false; } - /// - /// Checks whether or not a given inherits from a specified type. - /// - /// The target instance to check. + /// Checks whether or not a given inherits from a specified type. /// The full name of the type to check for inheritance. - /// Whether or not inherits from . - public static bool InheritsFromFullyQualifiedMetadataNameStartingWith(this ITypeSymbol typeSymbol, string name) + /// Whether the type symbol inherits from . + internal bool InheritsFromFullyQualifiedMetadataNameStartingWith(string name) { var baseType = typeSymbol.BaseType; @@ -117,13 +104,10 @@ public static bool InheritsFromFullyQualifiedMetadataNameStartingWith(this IType return false; } - /// - /// Checks whether or not a given has or inherits a specified attribute. - /// - /// The target instance to check. + /// Checks whether or not a given has or inherits a specified attribute. /// The name of the attribute to look for. - /// Whether or not has an attribute with the specified type name. - public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + /// Whether the type symbol has an attribute with the specified type name. + internal bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(string name) { for (var currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) { @@ -136,45 +120,50 @@ public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITy return false; } - /// - /// Checks whether or not a given type symbol has a specified fully qualified metadata name. - /// - /// The input instance to check. + /// Checks whether or not a given type symbol has a specified fully qualified metadata name. /// The full name to check. - /// Whether has a full name equals to . - public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name) + /// Whether the type symbol has a full name equal to . + internal bool HasFullyQualifiedMetadataName(string name) { using var builder = ImmutableArrayBuilder.Rent(); - symbol.AppendFullyQualifiedMetadataName(builder); + AppendFullyQualifiedMetadataName(typeSymbol, builder); return builder.WrittenSpan.StartsWith(name.AsSpan()); } - public static bool ContainsFullyQualifiedMetadataName(this ITypeSymbol symbol, string name) + /// Checks whether a type symbol's metadata name contains a given value. + /// The value to find in the full metadata name. + /// Whether the metadata name contains . + internal bool ContainsFullyQualifiedMetadataName(string name) { using var builder = ImmutableArrayBuilder.Rent(); - symbol.AppendFullyQualifiedMetadataName(builder); + AppendFullyQualifiedMetadataName(typeSymbol, builder); return builder.WrittenSpan.ToString().Contains(name); } - /// - /// Gets the fully qualified metadata name for a given instance. - /// - /// The input instance. - /// The fully qualified metadata name for . - public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol) + /// Gets the fully qualified metadata name for a given instance. + /// The fully qualified metadata name for the type symbol. + internal string GetFullyQualifiedMetadataName() { using var builder = ImmutableArrayBuilder.Rent(); - symbol.AppendFullyQualifiedMetadataName(builder); + AppendFullyQualifiedMetadataName(typeSymbol, builder); return builder.ToString(); } - public static bool IsTaskReturnType(this ITypeSymbol? typeSymbol) + } + + /// Provides null-tolerant type-classification operations. + /// The type symbol receiving the extension operation. + extension(ITypeSymbol? typeSymbol) + { + /// Determines whether a type symbol represents a task return type. + /// Whether the type symbol or a base type represents a task. + internal bool IsTaskReturnType() { var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; do @@ -187,12 +176,14 @@ public static bool IsTaskReturnType(this ITypeSymbol? typeSymbol) typeSymbol = typeSymbol?.BaseType; } - while (typeSymbol != null); + while (typeSymbol is not null); return false; } - public static bool IsObservableReturnType(this ITypeSymbol? typeSymbol) + /// Determines whether a type symbol represents an observable return type. + /// Whether the type symbol or a base type represents an observable. + internal bool IsObservableReturnType() { var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; do @@ -205,29 +196,37 @@ public static bool IsObservableReturnType(this ITypeSymbol? typeSymbol) typeSymbol = typeSymbol?.BaseType; } - while (typeSymbol != null); + while (typeSymbol is not null); return false; } - public static bool IsISchedulerType(this ITypeSymbol? typeSymbol) + /// Determines whether a type symbol represents the configured scheduler API. + /// The ReactiveUI API family to inspect. + /// Whether the type symbol or a base type represents that scheduler API. + internal bool IsSchedulerType(ReactiveUiApi api) { + var expectedTypeName = api == ReactiveUiApi.Primitives + ? "global::ReactiveUI.Primitives.Concurrency.ISequencer" + : "global::System.Reactive.Concurrency.IScheduler"; var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; do { var typeName = typeSymbol?.ToDisplayString(nameFormat); - if (typeName == "global::System.Reactive.Concurrency.IScheduler") + if (typeName == expectedTypeName) { return true; } typeSymbol = typeSymbol?.BaseType; } - while (typeSymbol != null); + while (typeSymbol is not null); return false; } - public static bool IsObservableBoolType(this ITypeSymbol? typeSymbol) + /// Determines whether a type symbol represents an observable Boolean value. + /// Whether the type symbol or a base type represents an observable Boolean. + internal bool IsObservableBoolType() { var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; do @@ -240,70 +239,68 @@ public static bool IsObservableBoolType(this ITypeSymbol? typeSymbol) typeSymbol = typeSymbol?.BaseType; } - while (typeSymbol != null); + while (typeSymbol is not null); return false; } - /// - /// Determines whether [is nullable type]. - /// - /// The type symbol. + /// Determines whether [is nullable type]. /// /// true if [is nullable type] [the specified type symbol]; otherwise, false. /// - public static bool IsNullableType(this ITypeSymbol? typeSymbol) => typeSymbol?.NullableAnnotation == NullableAnnotation.Annotated; + internal bool IsNullableType() => typeSymbol?.NullableAnnotation == NullableAnnotation.Annotated; - public static ITypeSymbol GetTaskReturnType(this ITypeSymbol typeSymbol, Compilation compilation) => typeSymbol switch + /// Gets the type produced by a task-like generic return type. + /// The current compilation. + /// The task result type, or when no result exists. + internal ITypeSymbol GetTaskReturnType(Compilation compilation) => typeSymbol switch { INamedTypeSymbol { TypeArguments.Length: 1 } namedTypeSymbol => namedTypeSymbol.TypeArguments[0], _ => compilation.GetSpecialType(SpecialType.System_Void) }; - /// - /// Appends the fully qualified metadata name for a given symbol to a target builder. - /// + } + + /// Appends the fully qualified metadata name for a given symbol to a target builder. /// The input instance. /// The target instance. - private static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ImmutableArrayBuilder builder) + private static void AppendFullyQualifiedMetadataName(ITypeSymbol symbol, ImmutableArrayBuilder builder) { - static void BuildFrom(ISymbol? symbol, ImmutableArrayBuilder builder) + static void BuildFrom(ISymbol? current, ImmutableArrayBuilder target) { - switch (symbol) + if (current is INamespaceSymbol namespaceSymbol) + { + if (!namespaceSymbol.IsGlobalNamespace) + { + if (!namespaceSymbol.ContainingNamespace.IsGlobalNamespace) + { + BuildFrom(namespaceSymbol.ContainingNamespace, target); + target.Add('.'); + } + + target.AddRange(namespaceSymbol.MetadataName.AsSpan()); + } + + return; + } + + if (current is not ITypeSymbol currentType) + { + return; + } + + if (currentType.ContainingSymbol is ITypeSymbol containingType) + { + BuildFrom(containingType, target); + target.Add('+'); + } + else if (currentType.ContainingSymbol is INamespaceSymbol containingNamespace && !containingNamespace.IsGlobalNamespace) { - // Namespaces that are nested also append a leading '.' - case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: - BuildFrom(symbol.ContainingNamespace, builder); - builder.Add('.'); - builder.AddRange(symbol.MetadataName.AsSpan()); - break; - - // Other namespaces (ie. the one right before global) skip the leading '.' - case INamespaceSymbol { IsGlobalNamespace: false }: - builder.AddRange(symbol.MetadataName.AsSpan()); - break; - - // Types with no namespace just have their metadata name directly written - case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }: - builder.AddRange(symbol.MetadataName.AsSpan()); - break; - - // Types with a containing non-global namespace also append a leading '.' - case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }: - BuildFrom(namespaceSymbol, builder); - builder.Add('.'); - builder.AddRange(symbol.MetadataName.AsSpan()); - break; - - // Nested types append a leading '+' - case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }: - BuildFrom(typeSymbol, builder); - builder.Add('+'); - builder.AddRange(symbol.MetadataName.AsSpan()); - break; - default: - break; + BuildFrom(containingNamespace, target); + target.Add('.'); } + + target.AddRange(currentType.MetadataName.AsSpan()); } BuildFrom(symbol, builder); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SymbolInfoExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SymbolInfoExtensions.cs index 59ecd9c4..e69c30fc 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SymbolInfoExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SymbolInfoExtensions.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; @@ -8,24 +7,18 @@ namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class SymbolInfoExtensions { - /// - /// Tries to get the resolved attribute type symbol from a given value. - /// - /// The value to check. - /// The resulting attribute type symbol, if correctly resolved. - /// Whether is resolved to a symbol. - /// - /// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just - /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore - /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. - /// - public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol) + /// Provides extension members for symbol information. + /// The symbol information to extend. + extension(SymbolInfo symbolInfo) { + /// Tries to get the resolved attribute type symbol from this value. + /// The resulting attribute type symbol, if correctly resolved. + /// Whether this value is resolved to a symbol. + internal bool TryGetAttributeTypeSymbol([NotNullWhen(true)] out INamedTypeSymbol? typeSymbol) + { var attributeSymbol = symbolInfo.Symbol; // If no symbol is selected and there is a single candidate symbol, use that @@ -45,5 +38,6 @@ public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNul typeSymbol = resultingSymbol; return true; + } } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxTokenExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxTokenExtensions.cs index 9c3fc732..332b254a 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxTokenExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxTokenExtensions.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; @@ -8,15 +7,15 @@ namespace ReactiveUI.SourceGenerators.Extensions; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class SyntaxTokenExtensions { - /// - /// Deconstructs a into its value. - /// - /// The input value. - /// The resulting value for . - public static void Deconstruct(this in SyntaxToken syntaxToken, out SyntaxKind syntaxKind) => syntaxKind = syntaxToken.Kind(); + /// Provides extension members for a syntax token. + /// The syntax token to extend. + extension(in SyntaxToken syntaxToken) + { + /// Deconstructs this token into its value. + /// The resulting value. + internal void Deconstruct(out SyntaxKind syntaxKind) => syntaxKind = syntaxToken.Kind(); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxValueProviderExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxValueProviderExtensions.cs index c8195b25..16981da9 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxValueProviderExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/SyntaxValueProviderExtensions.cs @@ -1,41 +1,30 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using System.Collections.Immutable; using System.Threading; using ReactiveUI.SourceGenerators.Extensions; namespace Microsoft.CodeAnalysis; -/// -/// Extension methods for the type. -/// +/// Extension methods for the type. internal static class SyntaxValueProviderExtensions { - /// - /// Creates an that can provide a transform over all s if that node has an attribute on it that binds to a with the - /// same fully-qualified metadata as the provided . should be the fully-qualified, metadata name of the attribute, including the - /// Attribute suffix. For example "System.CLSCompliantAttribute for . - /// - /// The source instance to use. - /// The fully qualified metadata name of the attribute to look for. - /// A function that determines if the given attribute target () should be transformed. Nodes that do not pass this - /// predicate will not have their attributes looked at all. - /// A function that performs the transform. This will only be passed nodes that return for and which have a matching whose - /// has the same fully qualified, metadata name as . - public static IncrementalValuesProvider ForAttributeWithMetadataNameWithGenerics( - this in SyntaxValueProvider syntaxValueProvider, - string fullyQualifiedMetadataName, - Func predicate, - Func transform) => syntaxValueProvider + /// Provides extension members for syntax value providers. + /// The syntax value provider to extend. + extension(in SyntaxValueProvider syntaxValueProvider) + { + /// Creates a provider for nodes with an attribute matching the supplied metadata name. + /// The generated value type. + /// The fully qualified metadata name of the attribute to look for. + /// Determines whether a syntax node should be inspected. + /// Transforms a matching attribute context into a result. + /// A provider that produces the transformed matching attribute contexts. + internal IncrementalValuesProvider ForAttributeWithMetadataNameWithGenerics( + string fullyQualifiedMetadataName, + Func predicate, + Func transform) => syntaxValueProvider .CreateSyntaxProvider( predicate, (context, token) => @@ -66,20 +55,24 @@ public static IncrementalValuesProvider ForAttributeWithMetadataNameWithGener // Create the GeneratorAttributeSyntaxContext value to pass to the input transform. The attributes array // will only ever have a single value, but that's fine with the attributes the various generators look for. +#if ROSYLN_412 || ROSYLN_500 + System.Collections.Immutable.ImmutableArray attributes = [attributeData]; +#else + var attributes = System.Collections.Immutable.ImmutableArray.Create(attributeData); +#endif GenericGeneratorAttributeSyntaxContext syntaxContext = new( targetNode: context.Node, targetSymbol: symbol, semanticModel: context.SemanticModel, - attributes: ImmutableArray.Create(attributeData)); + attributes); return new Option(transform(syntaxContext, token)); }) .Where(static item => item is not null) .Select(static (item, _) => item!.Value)!; + } - /// - /// A simple record to wrap a value that might be missing. - /// + /// A simple record to wrap a value that might be missing. /// The type of values to wrap. /// The wrapped value, if it exists. private sealed record Option(T? Value); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/AttributeInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/AttributeInfo.cs index 284d8c12..705bf20f 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/AttributeInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/AttributeInfo.cs @@ -1,11 +1,9 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,9 +12,7 @@ namespace ReactiveUI.SourceGenerators.Helpers; -/// -/// A model representing an attribute declaration. -/// +/// A model representing an attribute declaration. /// The type name of the attribute. /// The values for all constructor arguments for the attribute. /// The values for all named arguments for the attribute. @@ -25,12 +21,13 @@ internal sealed record AttributeInfo( EquatableArray ConstructorArgumentInfo, EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo) { - /// - /// Creates a new instance from a given value. - /// + /// + public override string ToString() => $"[{GetSyntax()}]"; + + /// Creates a new instance from a given value. /// The input value. /// A instance representing . - public static AttributeInfo Create(AttributeData attributeData) + internal static AttributeInfo Create(AttributeData attributeData) { var typeName = attributeData.AttributeClass!.GetFullyQualifiedName(); @@ -55,16 +52,14 @@ public static AttributeInfo Create(AttributeData attributeData) namedArguments.ToImmutable()); } - /// - /// Creates a new instance from a given syntax node. - /// + /// Creates a new instance from a given syntax node. /// The symbol for the attribute type. /// The instance for the current run. /// The sequence of instances to process. /// The cancellation token for the current operation. /// The resulting instance, if available. /// Whether a resulting instance could be created. - public static bool TryCreate( + internal static bool TryCreate( INamedTypeSymbol typeSymbol, SemanticModel semanticModel, IEnumerable arguments, @@ -104,7 +99,7 @@ public static bool TryCreate( } } - info = new AttributeInfo( + info = new( typeName, constructorArguments.ToImmutable(), namedArguments.ToImmutable()); @@ -112,25 +107,24 @@ public static bool TryCreate( return true; } - /// - /// Gets an instance representing the current value. - /// + /// Gets an instance representing the current value. /// The instance representing the current value. - public AttributeSyntax GetSyntax() + internal AttributeSyntax GetSyntax() { - // Gather the constructor arguments - var arguments = - ConstructorArgumentInfo - .Select(static arg => AttributeArgument(arg.GetSyntax())); - - // Gather the named arguments - var namedArguments = - NamedArgumentInfo.Select(static arg => - AttributeArgument(arg.Value.GetSyntax()) - .WithNameEquals(NameEquals(IdentifierName(arg.Name)))); - - return Attribute(IdentifierName(TypeName), AttributeArgumentList(SeparatedList(arguments.Concat(namedArguments)))); - } + using var arguments = ImmutableArrayBuilder.Rent(); - public override string ToString() => $"[{GetSyntax()}]"; + foreach (var constructorArgument in ConstructorArgumentInfo.AsImmutableArray()) + { + arguments.Add(AttributeArgument(constructorArgument.GetSyntax())); + } + + foreach (var namedArgument in NamedArgumentInfo.AsImmutableArray()) + { + arguments.Add( + AttributeArgument(namedArgument.Value.GetSyntax()) + .WithNameEquals(NameEquals(IdentifierName(namedArgument.Name)))); + } + + return Attribute(IdentifierName(TypeName), AttributeArgumentList(SeparatedList(arguments.ToImmutable()))); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArrayExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArrayExtensions.cs new file mode 100644 index 00000000..ec20f12d --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArrayExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Immutable; + +namespace ReactiveUI.SourceGenerators.Helpers; + +/// Extensions for . +internal static class EquatableArrayExtensions +{ + /// Provides extension members for an immutable array. + /// The type of items in the array. + /// The immutable array to extend. + extension(ImmutableArray array) + where T : IEquatable + { + /// Creates an instance from this array. + /// An instance. + internal EquatableArray AsEquatableArray() => new(array); + } +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs index 95c56a1d..b3befb5a 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,101 +7,78 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; namespace ReactiveUI.SourceGenerators.Helpers; -/// -/// An immutable, equatable array. This is equivalent to but with value equality support. -/// +/// An immutable, equatable array. This is equivalent to but with value equality support. /// The type of values in the array. -/// -/// Creates a new instance. -/// -/// The input to wrap. -internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable +internal readonly struct EquatableArray : IEquatable>, IEnumerable where T : IEquatable { - /// - /// The underlying array. - /// - private readonly T[]? _array = Unsafe.As, T[]?>(ref array); - - /// - /// Gets a value indicating whether the current array is empty. - /// - public bool IsEmpty + /// The underlying array. + private readonly T[]? _array; + + /// Initializes a new instance of the struct. + /// The immutable array to wrap. + internal EquatableArray(ImmutableArray array) => _array = Unsafe.As, T[]?>(ref array); + + /// Gets a value indicating whether the current array is empty. + internal bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => AsImmutableArray().IsEmpty; } - /// - /// Gets a reference to an item at a specified position within the array. - /// + /// Gets the number of values in the array. + internal int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _array?.Length ?? 0; + } + + /// Gets the number of values in the array. + internal int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Length; + } + + /// Gets a reference to an item at a specified position within the array. /// The index of the item to retrieve a reference to. /// A reference to an item at a specified position within the array. - public ref readonly T this[int index] + internal ref readonly T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref AsImmutableArray().ItemRef(index); } - /// - /// Implicitly converts an to . - /// + /// Implicitly converts an to . /// An instance from a given . public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); - /// - /// Implicitly converts an to . - /// + /// Implicitly converts an to . /// An instance from a given . public static implicit operator ImmutableArray(in EquatableArray array) => array.AsImmutableArray(); - /// - /// Checks whether two values are the same. - /// + /// Checks whether two values are the same. /// The first value. /// The second value. /// Whether and are equal. public static bool operator ==(in EquatableArray left, in EquatableArray right) => left.Equals(right); - /// - /// Checks whether two values are not the same. - /// + /// Checks whether two values are not the same. /// The first value. /// The second value. /// Whether and are not equal. public static bool operator !=(in EquatableArray left, in EquatableArray right) => !left.Equals(right); - /// - /// Creates an instance from a given . - /// - /// The input instance. - /// An instance from a given . - public static EquatableArray FromImmutableArray(ImmutableArray array) => new(array); - - /// - /// Equalses the specified array. - /// - /// The array. - /// A bool. -#pragma warning disable RCS1168 // Parameter name differs from base name - public bool Equals(EquatableArray array) => AsSpan().SequenceEqual(array.AsSpan()); -#pragma warning restore RCS1168 // Parameter name differs from base name - - /// - /// Equalses the specified object. - /// + /// Equalses the specified object. /// The object. /// A bool. public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(array); - /// - /// Gets the hash code. - /// + /// Gets the hash code. /// and int. public override int GetHashCode() { @@ -121,49 +97,40 @@ public override int GetHashCode() return hashCode.ToHashCode(); } - /// - /// Gets an instance from the current . - /// + /// Creates an instance from a given . + /// The input instance. + /// An instance from a given . + internal static EquatableArray FromImmutableArray(ImmutableArray array) => new(array); + + /// Equalses the specified array. + /// The array. + /// A bool. + internal bool Equals(EquatableArray other) => AsSpan().SequenceEqual(other.AsSpan()); + + bool IEquatable>.Equals(EquatableArray other) => Equals(other); + + /// Gets an instance from the current . /// The from the current . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ImmutableArray AsImmutableArray() => Unsafe.As>(ref Unsafe.AsRef(in _array)); + internal ImmutableArray AsImmutableArray() => Unsafe.As>(ref Unsafe.AsRef(in _array)); - /// - /// Returns a wrapping the current items. - /// + /// Creates an from the current array. + /// The immutable array represented by the current value. + internal ImmutableArray ToImmutableArray() => AsImmutableArray(); + + /// Returns a wrapping the current items. /// A wrapping the current items. - public ReadOnlySpan AsSpan() => AsImmutableArray().AsSpan(); + internal ReadOnlySpan AsSpan() => AsImmutableArray().AsSpan(); - /// - /// Copies the contents of this instance to a mutable array. - /// + /// Copies the contents of this instance to a mutable array. /// The newly instantiated array. - public T[] ToArray() => [.. AsImmutableArray()]; + internal T[] ToArray() => [.. AsImmutableArray()]; - /// - /// Gets an value to traverse items in the current array. - /// + /// Gets an value to traverse items in the current array. /// An value to traverse items in the current array. - public ImmutableArray.Enumerator GetEnumerator() => AsImmutableArray().GetEnumerator(); + internal ImmutableArray.Enumerator GetEnumerator() => AsImmutableArray().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator(); } - -/// -/// Extensions for . -/// -#pragma warning disable SA1402 // File may only contain a single type -internal static class EquatableArray -#pragma warning restore SA1402 // File may only contain a single type -{ - /// - /// Creates an instance from a given . - /// - /// The type of items in the input array. - /// The input instance. - /// An instance from a given . - public static EquatableArray AsEquatableArray(this ImmutableArray array) - where T : IEquatable => new(array); -} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/HashCode.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/HashCode.cs index 3a32d899..94c27ef8 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/HashCode.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/HashCode.cs @@ -1,60 +1,123 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.ComponentModel; using System.Runtime.CompilerServices; using System.Security.Cryptography; -#pragma warning disable CS0809 - namespace System; -/// -/// A polyfill type that mirrors some methods from on .NET 6. -/// -#pragma warning disable CA1066 // Implement IEquatable when overriding Object.Equals -internal struct HashCode -#pragma warning restore CA1066 // Implement IEquatable when overriding Object.Equals +/// A polyfill type that mirrors some methods from on .NET 6. +internal struct HashCode : IEquatable { - private const uint Prime1 = 2654435761U; - private const uint Prime2 = 2246822519U; - private const uint Prime3 = 3266489917U; - private const uint Prime4 = 668265263U; - private const uint Prime5 = 374761393U; + /// The number of values held in the state. + private const uint ValuesPerState = 4; + + /// The position of the first queued value. + private const uint FirstQueuePosition = 0; + + /// The position of the second queued value. + private const uint SecondQueuePosition = 1; + + /// The position of the third queued value. + private const uint ThirdQueuePosition = 2; + + /// The bit-width of the hash value. + private const int HashWidthInBits = 32; + + /// The first prime used by the xxHash algorithm. + private const uint Prime1 = 2_654_435_761U; + + /// The second prime used by the xxHash algorithm. + private const uint Prime2 = 2_246_822_519U; + + /// The third prime used by the xxHash algorithm. + private const uint Prime3 = 3_266_489_917U; + + /// The fourth prime used by the xxHash algorithm. + private const uint Prime4 = 668_265_263U; + + /// The fifth prime used by the xxHash algorithm. + private const uint Prime5 = 374_761_393U; + + /// The rotation offset for a round. + private const int RoundRotationOffset = 13; + + /// The rotation offset for a queued value. + private const int QueueRoundRotationOffset = 17; + + /// The rotation offset for the first state value. + private const int FirstStateRotationOffset = 1; + + /// The rotation offset for the second state value. + private const int SecondStateRotationOffset = 7; + + /// The rotation offset for the third state value. + private const int ThirdStateRotationOffset = 12; + + /// The rotation offset for the fourth state value. + private const int FourthStateRotationOffset = 18; + /// The process-specific hash seed. private static readonly uint seed = GenerateGlobalSeed(); + /// The first state value. private uint _v1; + + /// The second state value. private uint _v2; + + /// The third state value. private uint _v3; + + /// The fourth state value. private uint _v4; + + /// The first queued value. private uint _queue1; + + /// The second queued value. private uint _queue2; + + /// The third queued value. private uint _queue3; + + /// The number of values added to the hash. private uint _length; - /// - /// Adds a single value to the current hash. - /// + /// + public readonly bool Equals(HashCode other) => + _v1 == other._v1 + && _v2 == other._v2 + && _v3 == other._v3 + && _v4 == other._v4 + && _queue1 == other._queue1 + && _queue2 == other._queue2 + && _queue3 == other._queue3 + && _length == other._length; + + /// + public override readonly bool Equals(object? obj) => obj is HashCode other && Equals(other); + + /// + public override readonly int GetHashCode() => ToHashCode(); + + /// Adds a single value to the current hash. /// The type of the value to add into the hash code. /// The value to add into the hash code. - public void Add(T value) => Add(value?.GetHashCode() ?? 0); + internal void Add(T value) => Add(value?.GetHashCode() ?? 0); - /// - /// Gets the resulting hashcode from the current instance. - /// + /// Gets the resulting hashcode from the current instance. /// The resulting hashcode from the current instance. - public readonly int ToHashCode() + internal readonly int ToHashCode() { var length = _length; - var position = length % 4; - var hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + var position = length % ValuesPerState; + var hash = length < ValuesPerState ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); - hash += length * 4; + hash += length * ValuesPerState; - if (position > 0) + if (position != 0) { hash = QueueRound(hash, _queue1); @@ -62,7 +125,7 @@ public readonly int ToHashCode() { hash = QueueRound(hash, _queue2); - if (position > 2) + if (position > ThirdQueuePosition) { hash = QueueRound(hash, _queue3); } @@ -73,34 +136,18 @@ public readonly int ToHashCode() return (int)hash; } -#pragma warning disable CA1065 - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override readonly int GetHashCode() => throw new NotSupportedException(); - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override readonly bool Equals(object? obj) => throw new NotSupportedException(); -#pragma warning restore CA1065 - - /// - /// Rotates the specified value left by the specified number of bits. - /// Similar in behavior to the x86 instruction ROL. - /// + /// Rotates the specified value left by the specified number of bits. Similar in behavior to the x86 instruction ROL. /// The value to rotate. /// The number of bits to rotate by. /// Any value outside the range [0..31] is treated as congruent mod 32. /// The rotated value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> (32 - offset)); + private static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> (HashWidthInBits - offset)); - /// - /// Initializes the default seed. - /// + /// Initializes the default seed. /// A random seed. - private static unsafe uint GenerateGlobalSeed() + private static uint GenerateGlobalSeed() { var bytes = new byte[4]; @@ -112,6 +159,11 @@ private static unsafe uint GenerateGlobalSeed() return BitConverter.ToUInt32(bytes, 0); } + /// Initializes the four state values. + /// The first state value. + /// The second state value. + /// The third state value. + /// The fourth state value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) { @@ -121,18 +173,41 @@ private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v v4 = seed - Prime1; } + /// Applies a round of the xxHash algorithm. + /// The hash to update. + /// The input value. + /// The updated hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint Round(uint hash, uint input) => RotateLeft(hash + (input * Prime2), 13) * Prime1; + private static uint Round(uint hash, uint input) => RotateLeft(hash + (input * Prime2), RoundRotationOffset) * Prime1; + /// Applies a round for a queued value. + /// The hash to update. + /// The queued value. + /// The updated hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint QueueRound(uint hash, uint queuedValue) => RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; + private static uint QueueRound(uint hash, uint queuedValue) => RotateLeft(hash + (queuedValue * Prime3), QueueRoundRotationOffset) * Prime4; + /// Mixes the four state values into a hash. + /// The first state value. + /// The second state value. + /// The third state value. + /// The fourth state value. + /// The mixed hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixState(uint v1, uint v2, uint v3, uint v4) => RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + private static uint MixState(uint v1, uint v2, uint v3, uint v4) => + RotateLeft(v1, FirstStateRotationOffset) + + RotateLeft(v2, SecondStateRotationOffset) + + RotateLeft(v3, ThirdStateRotationOffset) + + RotateLeft(v4, FourthStateRotationOffset); + /// Creates a hash state with no added values. + /// The initialized hash state. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint MixEmptyState() => seed + Prime5; + /// Applies the final xxHash mixing operations. + /// The hash to mix. + /// The final mixed hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint MixFinal(uint hash) { @@ -145,27 +220,29 @@ private static uint MixFinal(uint hash) return hash; } + /// Adds a hash code to the current state. + /// The hash code to add. private void Add(int value) { var val = (uint)value; var previousLength = _length++; - var position = previousLength % 4; + var position = previousLength % ValuesPerState; - if (position == 0) + if (position == FirstQueuePosition) { _queue1 = val; } - else if (position == 1) + else if (position == SecondQueuePosition) { _queue2 = val; } - else if (position == 2) + else if (position == ThirdQueuePosition) { _queue3 = val; } else { - if (previousLength == 3) + if (previousLength == ValuesPerState - 1) { Initialize(out _v1, out _v2, out _v3, out _v4); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/ImmutableArrayBuilder{T}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/ImmutableArrayBuilder{T}.cs index e7495d6d..3514f446 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/ImmutableArrayBuilder{T}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/ImmutableArrayBuilder{T}.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -13,85 +12,72 @@ namespace ReactiveUI.SourceGenerators.Helpers; -/// -/// A helper type to build sequences of values with pooled buffers. -/// +/// A helper type to build sequences of values with pooled buffers. /// The type of items to create sequences for. internal ref struct ImmutableArrayBuilder { - /// - /// The rented instance to use. - /// + /// The rented instance to use. private Writer? _writer; - /// - /// Initializes a new instance of the struct. - /// + /// Initializes a new instance of the struct. /// The target data writer to use. private ImmutableArrayBuilder(Writer writer) => _writer = writer; - /// - /// Gets the data written to the underlying buffer so far, as a . - /// + /// Gets the data written to the underlying buffer so far, as a . [UnscopedRef] - public readonly ReadOnlySpan WrittenSpan + internal readonly ReadOnlySpan WrittenSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _writer!.WrittenSpan; } - /// - /// Gets the count. - /// + /// Gets the count. /// /// The count. /// - public readonly int Count + internal readonly int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _writer!.Count; } - /// - /// Creates a value with a pooled underlying data writer. - /// + /// + public override readonly string ToString() => _writer!.WrittenSpan.ToString(); + + /// Creates a value with a pooled underlying data writer. /// A instance to write data to. - public static ImmutableArrayBuilder Rent() => new(new Writer()); + internal static ImmutableArrayBuilder Rent() => new(new Writer()); - /// - public readonly void Add(T item) => _writer!.Add(item); + /// Adds an item to the builder. + /// The item to add. + internal readonly void Add(T item) => _writer!.Add(item); - /// - /// Adds the specified items to the end of the array. - /// + /// Adds the specified items to the end of the array. /// The items to add at the end of the array. - public readonly void AddRange(scoped in ReadOnlySpan items) => _writer!.AddRange(items); + internal readonly void AddRange(scoped in ReadOnlySpan items) => _writer!.AddRange(items); - /// - public readonly ImmutableArray ToImmutable() + /// Creates an immutable array from the written items. + /// An immutable array containing the written items. + internal readonly ImmutableArray ToImmutable() { var array = _writer!.WrittenSpan.ToArray(); return Unsafe.As>(ref array); } - /// - public readonly T[] ToArray() => _writer!.WrittenSpan.ToArray(); + /// Creates an array from the written items. + /// An array containing the written items. + internal readonly T[] ToArray() => _writer!.WrittenSpan.ToArray(); - /// - /// Gets an instance for the current builder. - /// + /// Gets an instance for the current builder. /// An instance for the current builder. /// /// The builder should not be mutated while an enumerator is in use. /// - public readonly IEnumerable AsEnumerable() => _writer!; - - /// - public override readonly string ToString() => _writer!.WrittenSpan.ToString(); + internal readonly IEnumerable AsEnumerable() => _writer!; - /// - public void Dispose() + /// Returns the pooled buffer to its array pool. + internal void Dispose() { var writer = _writer; @@ -100,23 +86,22 @@ public void Dispose() writer?.Dispose(); } - /// - /// A class handling the actual buffer writing. - /// + /// A class handling the actual buffer writing. private sealed class Writer : ICollection, IDisposable { - /// - /// The underlying array. - /// + /// The default number of pooled items to rent. + private const int DefaultCapacity = 8; + + /// The initial capacity used for character buffers. + private const int CharacterCapacity = 1024; + + /// The underlying array. private T?[]? _array; - /// - /// Initializes a new instance of the class. - /// Creates a new instance with the specified parameters. - /// + /// Initializes a new instance of the class. Creates a new instance with the specified parameters. public Writer() { - _array = ArrayPool.Shared.Rent(typeof(T) == typeof(char) ? 1024 : 8); + _array = ArrayPool.Shared.Rent(typeof(T) == typeof(char) ? CharacterCapacity : DefaultCapacity); Count = 0; } @@ -127,7 +112,7 @@ public int Count get; private set; } - /// + /// Gets the written items as a read-only span. public ReadOnlySpan WrittenSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -137,15 +122,18 @@ public ReadOnlySpan WrittenSpan /// bool ICollection.IsReadOnly => true; - /// + /// Adds an item to the writer. + /// The item to add. public void Add(T item) { EnsureCapacity(1); - _array![Count++] = item; + _array![Count] = item; + Count++; } - /// + /// Adds a range of items to the writer. + /// The items to add. public void AddRange(in ReadOnlySpan items) { EnsureCapacity(items.Length); @@ -162,10 +150,12 @@ public void Dispose() _array = null; - if (array is not null) + if (array is null) { - ArrayPool.Shared.Return(array, clearArray: typeof(T) != typeof(char)); + return; } + + ArrayPool.Shared.Return(array, clearArray: typeof(T) != typeof(char)); } /// @@ -195,22 +185,20 @@ IEnumerator IEnumerable.GetEnumerator() /// bool ICollection.Remove(T item) => throw new NotSupportedException(); - /// - /// Ensures that has enough free space to contain a given number of new items. - /// + /// Ensures that has enough free space to contain a given number of new items. /// The minimum number of items to ensure space for in . [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureCapacity(int requestedSize) { - if (requestedSize > _array!.Length - Count) + if (requestedSize <= _array!.Length - Count) { - ResizeBuffer(requestedSize); + return; } + + ResizeBuffer(requestedSize); } - /// - /// Resizes to ensure it can fit the specified number of new items. - /// + /// Resizes to ensure it can fit the specified number of new items. /// The minimum number of items to ensure space for in . [MethodImpl(MethodImplOptions.NoInlining)] private void ResizeBuffer(int sizeHint) diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/SymbolHelpers.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/SymbolHelpers.cs index f7b1dbc2..bd54cfbf 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/SymbolHelpers.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/SymbolHelpers.cs @@ -1,21 +1,16 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; namespace ReactiveUI.SourceGenerators.Helpers; -/// -/// Helper methods for working with symbols. -/// +/// Helper methods for working with symbols. internal static class SymbolHelpers { - /// - /// Default display format for symbols, omitting the global namespace and including nullable reference type modifiers. - /// - public static readonly SymbolDisplayFormat DefaultDisplay = + /// Default display format for symbols, omitting the global namespace and including nullable reference type modifiers. + internal static readonly SymbolDisplayFormat DefaultDisplay = SymbolDisplayFormat.FullyQualifiedFormat .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) .WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.Factory.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.Factory.cs index e8edadc8..a426ebbe 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.Factory.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.Factory.cs @@ -1,12 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -15,15 +13,13 @@ namespace ReactiveUI.SourceGenerators.Helpers; /// -internal partial record TypedConstantInfo +internal abstract partial record TypedConstantInfo { - /// - /// Creates a new instance from a given value. - /// + /// Creates a new instance from a given value. /// The input value. /// A instance representing . /// Thrown if the input argument is not valid. - public static TypedConstantInfo Create(TypedConstant arg) + internal static TypedConstantInfo Create(TypedConstant arg) { if (arg.IsNull) { @@ -33,9 +29,13 @@ public static TypedConstantInfo Create(TypedConstant arg) if (arg.Kind == TypedConstantKind.Array) { var elementTypeName = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var items = arg.Values.Select(Create).ToImmutableArray(); + using var items = ImmutableArrayBuilder.Rent(); + foreach (var value in arg.Values) + { + items.Add(Create(value)); + } - return new Array(elementTypeName, items); + return new Array(elementTypeName, items.ToImmutable()); } return (arg.Kind, arg.Value) switch @@ -63,9 +63,7 @@ public static TypedConstantInfo Create(TypedConstant arg) }; } - /// - /// Creates a new instance from a given value. - /// + /// Creates a new instance from a given value. /// The input value. /// The that was used to retrieve . /// The that was retrieved from. @@ -73,43 +71,15 @@ public static TypedConstantInfo Create(TypedConstant arg) /// The resulting instance, if available. /// Whether a resulting instance could be created. /// Thrown if the input argument is not valid. - public static bool TryCreate( + internal static bool TryCreate( IOperation operation, SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken token, [NotNullWhen(true)] out TypedConstantInfo? info) { - if (operation.ConstantValue.HasValue) + if (TryCreateConstant(operation, out info)) { - // Enum values are constant but need to be checked explicitly in this case - if (operation.Type?.TypeKind is TypeKind.Enum) - { - info = new Enum(operation.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), operation.ConstantValue.Value!); - - return true; - } - - // Handle all other constant literals normally - info = operation.ConstantValue.Value switch - { - null => new Null(), - string text => new Primitive.String(text), - bool flag => new Primitive.Boolean(flag), - byte b => new Primitive.Of(b), - char c => new Primitive.Of(c), - double d => new Primitive.Of(d), - float f => new Primitive.Of(f), - int i => new Primitive.Of(i), - long l => new Primitive.Of(l), - sbyte sb => new Primitive.Of(sb), - short sh => new Primitive.Of(sh), - uint ui => new Primitive.Of(ui), - ulong ul => new Primitive.Of(ul), - ushort ush => new Primitive.Of(ush), - _ => throw new ArgumentException("Invalid primitive type") - }; - return true; } @@ -120,51 +90,126 @@ public static bool TryCreate( return true; } - if (operation is IArrayCreationOperation) + if (operation is IArrayCreationOperation && TryCreateArray(operation, semanticModel, expression, token, out info)) { - var elementTypeName = ((IArrayTypeSymbol?)operation.Type)?.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - // If the element type is not available (since the attribute wasn't checked), just default to object - elementTypeName ??= "object"; + return true; + } - var initializerExpression = - (expression as ImplicitArrayCreationExpressionSyntax)?.Initializer - ?? (expression as ArrayCreationExpressionSyntax)?.Initializer; + info = null; - // No initializer found, just return an empty array - if (initializerExpression is null) - { - info = new Array(elementTypeName, ImmutableArray.Empty); + return false; + } - return true; - } + /// Creates a typed constant from a constant operation. + /// The operation to inspect. + /// The resulting constant information, when successful. + /// when the operation has a supported constant value. + private static bool TryCreateConstant( + IOperation operation, + [NotNullWhen(true)] out TypedConstantInfo? info) + { + if (!operation.ConstantValue.HasValue) + { + info = null; + return false; + } - using var items = ImmutableArrayBuilder.Rent(); + if (operation.Type?.TypeKind is TypeKind.Enum) + { + info = new Enum( + operation.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + operation.ConstantValue.Value!); + return true; + } - // Enumerate all array elements and extract serialized info for them - foreach (var initializationExpression in initializerExpression.Expressions) - { - if (semanticModel.GetOperation(initializationExpression, token) is not IOperation initializationOperation) - { - goto Failure; - } + info = operation.ConstantValue.Value switch + { + null => new Null(), + string text => new Primitive.String(text), + bool flag => new Primitive.Boolean(flag), + byte byteValue => new Primitive.Of(byteValue), + char characterValue => new Primitive.Of(characterValue), + double doubleValue => new Primitive.Of(doubleValue), + float singleValue => new Primitive.Of(singleValue), + int integerValue => new Primitive.Of(integerValue), + long longValue => new Primitive.Of(longValue), + sbyte signedByteValue => new Primitive.Of(signedByteValue), + short shortValue => new Primitive.Of(shortValue), + uint unsignedIntegerValue => new Primitive.Of(unsignedIntegerValue), + ulong unsignedLongValue => new Primitive.Of(unsignedLongValue), + ushort unsignedShortValue => new Primitive.Of(unsignedShortValue), + _ => throw new ArgumentException("Invalid primitive type"), + }; + return true; + } - if (!TryCreate(initializationOperation, semanticModel, initializationExpression, token, out var elementInfo)) - { - goto Failure; - } + /// Creates a typed constant from an array operation. + /// The array operation to inspect. + /// The semantic model containing the operation. + /// The array expression. + /// The cancellation token for the current operation. + /// The resulting constant information, when successful. + /// when all array items can be represented. + private static bool TryCreateArray( + IOperation operation, + SemanticModel semanticModel, + ExpressionSyntax expression, + CancellationToken token, + [NotNullWhen(true)] out TypedConstantInfo? info) + { + var elementTypeName = ((IArrayTypeSymbol?)operation.Type)?.ElementType + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? "object"; + var initializer = GetInitializer(expression); + if (initializer is null) + { + info = new Array(elementTypeName, ImmutableArray.Empty); + return true; + } - items.Add(elementInfo); + using var items = ImmutableArrayBuilder.Rent(); + foreach (var itemExpression in initializer.Expressions) + { + if (!TryCreateArrayItem(itemExpression, semanticModel, token, out var item)) + { + info = null; + return false; } - info = new Array(elementTypeName, items.ToImmutable()); - - return true; + items.Add(item); } - Failure: - info = null; + info = new Array(elementTypeName, items.ToImmutable()); + return true; + } + + /// Gets the initializer from an explicit or implicit array expression. + /// The expression to inspect. + /// The array initializer, if present. + private static InitializerExpressionSyntax? GetInitializer(ExpressionSyntax expression) => expression switch + { + ImplicitArrayCreationExpressionSyntax implicitArray => implicitArray.Initializer, + ArrayCreationExpressionSyntax array => array.Initializer, + _ => null, + }; + + /// Creates typed constant information for an array item. + /// The array item expression. + /// The semantic model containing the expression. + /// The cancellation token for the current operation. + /// The resulting item information, when successful. + /// when the item can be represented. + private static bool TryCreateArrayItem( + ExpressionSyntax expression, + SemanticModel semanticModel, + CancellationToken token, + [NotNullWhen(true)] out TypedConstantInfo? item) + { + if (semanticModel.GetOperation(expression, token) is IOperation operation) + { + return TryCreate(operation, semanticModel, expression, token, out item); + } + item = null; return false; } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.cs index 923102bb..bfe6cd73 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/TypedConstantInfo.cs @@ -1,11 +1,9 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Globalization; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -13,91 +11,108 @@ namespace ReactiveUI.SourceGenerators.Helpers; -/// -/// A model representing a typed constant item. -/// +/// A model representing a typed constant item. /// This model is fully serializable and comparable. internal abstract partial record TypedConstantInfo { - /// - /// Gets an instance representing the current constant. - /// + /// Gets an instance representing the current constant. /// The instance representing the current constant. - public abstract ExpressionSyntax GetSyntax(); + internal abstract ExpressionSyntax GetSyntax(); - /// - /// A type representing an array. - /// + /// Creates syntax expressions for an array of typed constants. + /// The typed constants to convert. + /// The corresponding syntax expressions. + private static ExpressionSyntax[] GetItemSyntaxes(EquatableArray items) + { + var itemSyntaxes = new ExpressionSyntax[items.Length]; + for (var index = 0; index < items.Length; index++) + { + itemSyntaxes[index] = items[index].GetSyntax(); + } + + return itemSyntaxes; + } + + /// Creates a numeric literal token for a primitive value. + /// The primitive value type. + /// The primitive value. + /// The matching numeric literal token. + private static SyntaxToken GetNumericLiteral(T value) + where T : unmanaged, IEquatable => value switch + { + byte byteValue => Literal(byteValue), + char characterValue => Literal(characterValue), + double doubleValue => Literal($"{doubleValue.ToString("R", CultureInfo.InvariantCulture)}D", doubleValue), + float singleValue => Literal(singleValue), + int integerValue => Literal(integerValue), + long longValue => Literal(longValue), + sbyte signedByteValue => Literal(signedByteValue), + short shortValue => Literal(shortValue), + uint unsignedIntegerValue => Literal(unsignedIntegerValue), + ulong unsignedLongValue => Literal(unsignedLongValue), + ushort unsignedShortValue => Literal(unsignedShortValue), + _ => throw new ArgumentException("Invalid primitive type"), + }; + + /// A type representing an array. /// The type name for array elements. /// The sequence of contained elements. - public sealed record Array(string ElementTypeName, EquatableArray Items) : TypedConstantInfo + internal sealed record Array(string ElementTypeName, EquatableArray Items) : TypedConstantInfo { /// - public override ExpressionSyntax GetSyntax() => ArrayCreationExpression( + internal override ExpressionSyntax GetSyntax() => ArrayCreationExpression( ArrayType(IdentifierName(ElementTypeName)) .AddRankSpecifiers(ArrayRankSpecifier(SingletonSeparatedList(OmittedArraySizeExpression())))) .WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression) - .AddExpressions(Items.Select(static c => c.GetSyntax()).ToArray())); + .AddExpressions(GetItemSyntaxes(Items))); } - /// - /// A type representing a primitive value. - /// - public abstract record Primitive : TypedConstantInfo + /// A type representing a primitive value. + internal abstract record Primitive : TypedConstantInfo { - /// - /// A type representing a value. - /// + /// A type representing a value. /// The input value. - public sealed record String(string Value) : TypedConstantInfo + internal sealed record String(string Value) : TypedConstantInfo { /// - public override ExpressionSyntax GetSyntax() => LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(Value)); + internal override ExpressionSyntax GetSyntax() => LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(Value)); } - /// - /// A type representing a value. - /// + /// A type representing a value. /// The input value. - public sealed record Boolean(bool Value) : TypedConstantInfo + internal sealed record Boolean(bool Value) : TypedConstantInfo { /// - public override ExpressionSyntax GetSyntax() => LiteralExpression(Value ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression); + internal override ExpressionSyntax GetSyntax() => LiteralExpression(Value ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression); } - /// - /// A type representing a generic primitive value. - /// + /// A type representing a generic primitive value. /// The primitive type. /// The input primitive value. - public sealed record Of(T Value) : TypedConstantInfo + internal sealed record Of(T Value) : TypedConstantInfo where T : unmanaged, IEquatable { /// - public override ExpressionSyntax GetSyntax() => - LiteralExpression(SyntaxKind.NumericLiteralExpression, Value switch { byte b => Literal(b), char c => Literal(c), double d => Literal(d.ToString("R", CultureInfo.InvariantCulture) + "D", d), float f => Literal(f), int i => Literal(i), long l => Literal(l), sbyte sb => Literal(sb), short sh => Literal(sh), uint ui => Literal(ui), ulong ul => Literal(ul), ushort ush => Literal(ush), _ => throw new ArgumentException("Invalid primitive type") }); + internal override ExpressionSyntax GetSyntax() => + LiteralExpression(SyntaxKind.NumericLiteralExpression, GetNumericLiteral(Value)); } } - /// - /// A type representing a type. - /// + /// A type representing a type. /// The input type name. - public sealed record Type(string TypeName) : TypedConstantInfo + internal sealed record Type(string TypeName) : TypedConstantInfo { /// - public override ExpressionSyntax GetSyntax() => TypeOfExpression(IdentifierName(TypeName)); + internal override ExpressionSyntax GetSyntax() => TypeOfExpression(IdentifierName(TypeName)); } - /// - /// A type representing an enum value. - /// + /// A type representing an enum value. /// The enum type name. /// The boxed enum value. - public sealed record Enum(string TypeName, object Value) : TypedConstantInfo + internal sealed record Enum(string TypeName, object Value) : TypedConstantInfo { /// - public override ExpressionSyntax GetSyntax() + internal override ExpressionSyntax GetSyntax() { // We let Roslyn parse the value expression, so that it can automatically handle both positive and negative values. This // is needed because negative values have a different syntax tree (UnaryMinusExpression holding the numeric expression). @@ -114,12 +129,10 @@ public override ExpressionSyntax GetSyntax() } } - /// - /// A type representing a value. - /// - public sealed record Null : TypedConstantInfo + /// A type representing a value. + internal sealed record Null : TypedConstantInfo { /// - public override ExpressionSyntax GetSyntax() => LiteralExpression(SyntaxKind.NullLiteralExpression); + internal override ExpressionSyntax GetSyntax() => LiteralExpression(SyntaxKind.NullLiteralExpression); } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/DiagnosticInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/DiagnosticInfo.cs index 2e531224..37fd7523 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/DiagnosticInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/DiagnosticInfo.cs @@ -1,19 +1,14 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model for a serializeable diagnostic info. -/// +/// A model for a serializeable diagnostic info. /// The wrapped instance. /// The tree to use as location for the diagnostic, if available. /// The span to use as location for the diagnostic. @@ -24,45 +19,48 @@ internal sealed record DiagnosticInfo( TextSpan TextSpan, EquatableArray Arguments) { - /// - /// Creates a new instance with the state from this model. - /// - /// A new instance with the state from this model. - public Diagnostic ToDiagnostic() - { - if (SyntaxTree is not null) - { - return Diagnostic.Create(Descriptor, Location.Create(SyntaxTree, TextSpan), Arguments.ToArray()); - } - - return Diagnostic.Create(Descriptor, null, Arguments.ToArray()); - } - - /// - /// Creates a new instance with the specified parameters. - /// + /// Creates a new instance with the specified parameters. /// The input for the diagnostics to create. /// The source to attach the diagnostics to. /// The optional arguments for the formatted message to include. /// A new instance with the specified parameters. - public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, ISymbol symbol, params object[] args) + internal static DiagnosticInfo Create(DiagnosticDescriptor descriptor, ISymbol symbol, params object[] args) { var location = symbol.Locations[0]; - return new(descriptor, location.SourceTree, location.SourceSpan, args.Select(static arg => arg.ToString()).ToImmutableArray()); + return new(descriptor, location.SourceTree, location.SourceSpan, CreateArguments(args)); } - /// - /// Creates a new instance with the specified parameters. - /// + /// Creates a new instance with the specified parameters. /// The input for the diagnostics to create. /// The source to attach the diagnostics to. /// The optional arguments for the formatted message to include. /// A new instance with the specified parameters. - public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, SyntaxNode node, params object[] args) + internal static DiagnosticInfo Create(DiagnosticDescriptor descriptor, SyntaxNode node, params object[] args) { var location = node.GetLocation(); - return new(descriptor, location.SourceTree, location.SourceSpan, args.Select(static arg => arg.ToString()).ToImmutableArray()); + return new(descriptor, location.SourceTree, location.SourceSpan, CreateArguments(args)); + } + + /// Creates a new instance with the state from this model. + /// A new instance with the state from this model. + internal Diagnostic ToDiagnostic() => + SyntaxTree is not null + ? Diagnostic.Create(Descriptor, Location.Create(SyntaxTree, TextSpan), Arguments.ToArray()) + : Diagnostic.Create(Descriptor, null, Arguments.ToArray()); + + /// Converts diagnostic arguments into an equatable array. + /// The diagnostic arguments. + /// The string representation of every argument. + private static EquatableArray CreateArguments(object[] args) + { + using var arguments = ImmutableArrayBuilder.Rent(); + foreach (var argument in args) + { + arguments.Add(argument.ToString() ?? string.Empty); + } + + return arguments.ToImmutable(); } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/GenericGeneratorAttributeSyntaxContext.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/GenericGeneratorAttributeSyntaxContext.cs index 75b41b9e..be584762 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/GenericGeneratorAttributeSyntaxContext.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/GenericGeneratorAttributeSyntaxContext.cs @@ -1,21 +1,17 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Collections.Immutable; +using HashCode = System.HashCode; namespace Microsoft.CodeAnalysis; -/// -/// A type containing information for a match from . -/// -internal readonly struct GenericGeneratorAttributeSyntaxContext +/// A type containing information for a match from . +internal readonly struct GenericGeneratorAttributeSyntaxContext : IEquatable { - /// - /// Initializes a new instance of the struct. - /// Creates a new instance with the specified parameters. - /// + /// Initializes a new instance of the struct. /// The syntax node the attribute is attached to. /// The symbol that the attribute is attached to. /// Semantic model for the file that is contained within. @@ -35,25 +31,44 @@ internal GenericGeneratorAttributeSyntaxContext( /// /// Gets the syntax node the attribute is attached to. For example, with [CLSCompliant] class C { } this would the class declaration node. /// - public SyntaxNode TargetNode { get; } + internal SyntaxNode TargetNode { get; } /// /// Gets the symbol that the attribute is attached to. For example, with [CLSCompliant] class C { } this would be the for "C". /// - public ISymbol TargetSymbol { get; } + internal ISymbol TargetSymbol { get; } - /// - /// Gets semantic model for the file that is contained within. - /// - public SemanticModel SemanticModel { get; } + /// Gets semantic model for the file that is contained within. + internal SemanticModel SemanticModel { get; } /// - /// Gets s for any matching attributes on . Always non-empty. All - /// these attributes will have an whose fully qualified name metadata - /// name matches the name requested in . + /// Gets the matching attributes on . This collection is always non-empty. Each attribute has an + /// whose fully qualified metadata name matches the requested name. /// /// To get the entire list of attributes, use on . /// /// - public ImmutableArray Attributes { get; } + internal ImmutableArray Attributes { get; } + + /// + public bool Equals(GenericGeneratorAttributeSyntaxContext other) => + ReferenceEquals(TargetNode, other.TargetNode) + && SymbolEqualityComparer.Default.Equals(TargetSymbol, other.TargetSymbol) + && ReferenceEquals(SemanticModel, other.SemanticModel) + && Attributes.Equals(other.Attributes); + + /// + public override bool Equals(object? obj) => + obj is GenericGeneratorAttributeSyntaxContext other && Equals(other); + + /// + public override int GetHashCode() + { + HashCode hashCode = default; + hashCode.Add(TargetNode); + hashCode.Add(SymbolEqualityComparer.Default.GetHashCode(TargetSymbol)); + hashCode.Add(SemanticModel); + hashCode.Add(Attributes); + return hashCode.ToHashCode(); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/PropertyAttributeData.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/PropertyAttributeData.cs index fc83987b..92b17675 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/PropertyAttributeData.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/PropertyAttributeData.cs @@ -1,8 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerators.Models; -internal record PropertyAttributeData(string? AttributeNamespace, string AttributeSyntax); +/// Represents the generated namespace and syntax for a property attribute. +/// The optional namespace containing the attribute. +/// The generated attribute syntax. +internal sealed record PropertyAttributeData(string? AttributeNamespace, string AttributeSyntax); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/ReactiveUiApi.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/ReactiveUiApi.cs new file mode 100644 index 00000000..db40d1a1 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/ReactiveUiApi.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.SourceGenerators.Models; + +/// Identifies the ReactiveUI implementation API selected by a compilation. +internal enum ReactiveUiApi +{ + /// ReactiveUI releases before the package split. + Legacy, + + /// The ReactiveUI 24 or later Primitives-based package. + Primitives, + + /// The ReactiveUI 24 or later System.Reactive-based package. + SystemReactive, +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/ReactiveUiIntegration.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/ReactiveUiIntegration.cs new file mode 100644 index 00000000..bd7d88ea --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/ReactiveUiIntegration.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.SourceGenerators.Models; + +/// Describes the ReactiveUI API surface referenced by a compilation. +/// The implementation API selected by the compilation. +/// Whether the compilation references ReactiveUI 22 or later. +internal readonly record struct ReactiveUiIntegration(ReactiveUiApi Api, bool IsNewerThan22) +{ + /// Gets the namespace containing the selected ReactiveUI implementation types. + internal string Namespace => Api == ReactiveUiApi.SystemReactive + ? "global::ReactiveUI.Reactive" + : "global::ReactiveUI"; + + /// Gets the non-global namespace used in generated declarations that historically omitted the global alias qualifier. + internal string DeclarationNamespace => Api == ReactiveUiApi.SystemReactive + ? "ReactiveUI.Reactive" + : "ReactiveUI"; + + /// Gets the type used for an empty command input or output. + internal string VoidTypeName => Api == ReactiveUiApi.Primitives + ? "global::ReactiveUI.Primitives.RxVoid" + : "global::System.Reactive.Unit"; + + /// Gets the using directives needed for implementation types and the common interfaces. + internal string UsingDirectives => Api == ReactiveUiApi.SystemReactive + ? "using ReactiveUI;\nusing ReactiveUI.Reactive;" + : "using ReactiveUI;"; +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/Result.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/Result.cs index ff9a26fa..edd865e8 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/Result.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/Result.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,9 +7,7 @@ namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model representing a value and an associated set of diagnostic errors. -/// +/// A model representing a value and an associated set of diagnostic errors. /// The type of the wrapped value. /// The wrapped value for the current result. /// The associated diagnostic errors, if any. diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/TargetInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/TargetInfo.cs index 29414896..7c1e779d 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/TargetInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Models/TargetInfo.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -10,7 +9,15 @@ namespace ReactiveUI.SourceGenerators.Models; -internal sealed partial record TargetInfo( +/// Describes a target type for generated source output. +/// The generated file hint name. +/// The target type name. +/// The target namespace. +/// The fully qualified target type name. +/// The target type visibility. +/// The target type keyword. +/// The containing type, if the target is nested. +internal sealed record TargetInfo( string FileHintName, string TargetName, string TargetNamespace, @@ -19,7 +26,10 @@ internal sealed partial record TargetInfo( string TargetType, TargetInfo? ParentInfo) { - public static TargetInfo From(INamedTypeSymbol namedTypeSymbol) + /// Creates target information from a named type symbol. + /// The target type symbol. + /// The generated target information. + internal static TargetInfo From(INamedTypeSymbol namedTypeSymbol) { var targetHintName = namedTypeSymbol.GetFullyQualifiedMetadataName().Replace("<", "_").Replace(">", "_"); var targetName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); @@ -42,7 +52,10 @@ public static TargetInfo From(INamedTypeSymbol namedTypeSymbol) parentInfo); } - public static (string Declarations, string ClosingBrackets) GenerateParentClassDeclarations(TargetInfo?[] targetInfos) + /// Generates the containing type declarations and corresponding closing braces. + /// The target types whose parents should be generated. + /// The declarations and closing braces. + internal static (string Declarations, string ClosingBrackets) GenerateParentClassDeclarations(TargetInfo?[] targetInfos) { var parentClassDeclarations = new List(); foreach (var targetInfo in targetInfos) @@ -55,26 +68,36 @@ public static (string Declarations, string ClosingBrackets) GenerateParentClassD return (parentClassDeclarationsString, closingBrackets); } + /// Adds all containing type declarations for a target type. + /// The declarations to populate. + /// The target whose parents are processed. private static void GetParentClasses(List parentClassDeclarations, TargetInfo? targetInfo) { - if (targetInfo is not null) + if (targetInfo is null) { - var parentClassDeclaration = $"{targetInfo.TargetVisibility} partial {targetInfo.TargetType} {targetInfo.TargetName}"; - - // Add the parent class declaration if it does not exist in the list - if (!parentClassDeclarations.Contains(parentClassDeclaration)) - { - parentClassDeclarations.Add(parentClassDeclaration); - } - - if (targetInfo.ParentInfo is not null) - { - // Recursively get the parent classes - GetParentClasses(parentClassDeclarations, targetInfo.ParentInfo); - } + return; } + + var parentClassDeclaration = $"{targetInfo.TargetVisibility} partial {targetInfo.TargetType} {targetInfo.TargetName}"; + + // Add the parent class declaration if it does not exist in the list + if (!parentClassDeclarations.Contains(parentClassDeclaration)) + { + parentClassDeclarations.Add(parentClassDeclaration); + } + + if (targetInfo.ParentInfo is null) + { + return; + } + + // Recursively get the parent classes + GetParentClasses(parentClassDeclarations, targetInfo.ParentInfo); } + /// Generates the text for the supplied containing type declarations. + /// The containing type declarations. + /// The generated declaration text. private static string GenerateParentClassDeclarations(List parentClassDeclarations) { // Reverse the list to get the parent classes in the correct order @@ -90,13 +113,16 @@ private static string GenerateParentClassDeclarations(List parentClassDe return parentClassDeclarationsString; } + /// Generates closing braces for a nesting depth. + /// The nesting depth. + /// The generated closing brace text. private static string GenerateClosingBrackets(int numberOfBrackets) { var closingBrackets = new string('}', numberOfBrackets); closingBrackets = closingBrackets.Replace("}", "}\n"); if (!string.IsNullOrWhiteSpace(closingBrackets)) { - closingBrackets = "\n" + closingBrackets; + closingBrackets = $"\n{closingBrackets}"; } return closingBrackets; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs index f2571910..386ea367 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs @@ -1,29 +1,26 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; -#pragma warning disable IDE0090 // Use 'new DiagnosticDescriptor(...)' - namespace ReactiveUI.SourceGenerators.Diagnostics; -/// -/// A container for all instances for errors reported by analyzers in this project. -/// +/// A container for all instances for errors reported by analyzers in this project. internal static class DiagnosticDescriptors { /// /// Gets a indicating when a generated property created with [Reactive] would collide with the source field. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its name would collide with the field name (instance fields should use the "lowerCamel", "_lowerCamel" or "m_lowerCamel" pattern). + /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its name would collide with the field name. + /// Instance fields should use the lowerCamel, _lowerCamel, or m_lowerCamel pattern. /// /// - public static readonly DiagnosticDescriptor ReactivePropertyNameCollisionError = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor ReactivePropertyNameCollisionError = new( id: "RXUISG0009", title: "Name collision for generated property", - messageFormat: "The field {0}.{1} cannot be used to generate an reactive property, as its name would collide with the field name (instance fields should use the \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern)", + messageFormat: "The field {0}.{1} cannot be used to generate an reactive property, as its name would collide with the field name " + + "(instance fields should use the \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern)", category: typeof(ReactiveGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, @@ -36,7 +33,7 @@ internal static class DiagnosticDescriptors /// Format: "The field {0} annotated with [Reactive] is using attribute "{1}" which was not recognized as a valid type (are you missing a using directive?)". /// /// - public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeOnReactiveField = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeOnReactiveField = new( id: "RXUISG0010", title: "Invalid property targeted attribute type", messageFormat: "The field {0} annotated with [Reactive] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", @@ -52,7 +49,7 @@ internal static class DiagnosticDescriptors /// Format: "The field {0} annotated with [Reactive] is using attribute "{1}" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)". /// /// - public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeExpressionOnReactiveField = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeExpressionOnReactiveField = new( id: "RXUISG0011", title: "Invalid property targeted attribute expression", messageFormat: "The field {0} annotated with [Reactive] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", @@ -68,7 +65,7 @@ internal static class DiagnosticDescriptors /// Format: "The field {0} annotated with [ObservableAsProperty] is using attribute "{1}" which was not recognized as a valid type (are you missing a using directive?)". /// /// - public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeOnObservableAsPropertyField = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeOnObservableAsPropertyField = new( id: "RXUISG0012", title: "Invalid property targeted attribute type", messageFormat: "The field {0} annotated with [ObservableAsProperty] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", @@ -81,13 +78,15 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when a field with [ObservableAsProperty] is using an invalid attribute expression targeting the property. /// - /// Format: "The field {0} annotated with [ObservableAsProperty] is using attribute "{1}" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)". + /// Format: "The field {0} annotated with [ObservableAsProperty] is using attribute "{1}" with an invalid expression. + /// The attribute constructor parameters might be incorrect. /// /// - public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField = new( id: "RXUISG0013", title: "Invalid property targeted attribute expression", - messageFormat: "The field {0} annotated with [ObservableAsProperty] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", + messageFormat: "The field {0} annotated with [ObservableAsProperty] is using attribute \"{1}\" with an invalid expression " + + "(are you passing any incorrect parameters to the attribute constructor?)", category: "ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, @@ -100,7 +99,7 @@ internal static class DiagnosticDescriptors /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". /// /// - public static readonly DiagnosticDescriptor InvalidObservableAsPropertyError = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor InvalidObservableAsPropertyError = new( id: "RXUISG0014", title: "Invalid generated property declaration", messageFormat: "The field {0}.{1} cannot be used to generate an observable As property, as its name or type would cause conflicts with other generated members", @@ -116,7 +115,7 @@ internal static class DiagnosticDescriptors /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". /// /// - public static readonly DiagnosticDescriptor InvalidReactiveError = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor InvalidReactiveError = new( id: "RXUISG0015", title: "Invalid generated property declaration", messageFormat: "The field {0}.{1} cannot be used to generate an reactive property, as its name or type would cause conflicts with other generated members", @@ -126,10 +125,8 @@ internal static class DiagnosticDescriptors description: "The fields annotated with [Reactive] cannot result in a property name or have a type that would cause conflicts with other generated members.", helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); - /// - /// The observable as property method has parameters error. - /// - public static readonly DiagnosticDescriptor ObservableAsPropertyMethodHasParametersError = new DiagnosticDescriptor( + /// The observable as property method has parameters error. + internal static readonly DiagnosticDescriptor ObservableAsPropertyMethodHasParametersError = new( id: "RXUISG0017", title: "Invalid generated property declaration", messageFormat: "The method {0} cannot be used to generate an observable As property, as it has parameters", @@ -139,10 +136,8 @@ internal static class DiagnosticDescriptors description: "The method annotated with [ObservableAsProperty] cannot currently initialize methods with parameters.", helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); - /// - /// The invalid reactive object error. - /// - public static readonly DiagnosticDescriptor InvalidReactiveObjectError = new DiagnosticDescriptor( + /// The invalid reactive object error. + internal static readonly DiagnosticDescriptor InvalidReactiveObjectError = new( id: "RXUISG0018", title: "Invalid class, does not inherit ReactiveObject", messageFormat: "The field {0}.{1} cannot be used to generate an ReactiveUI property, as it is not part of a class that inherits from ReactiveObject", @@ -152,10 +147,8 @@ internal static class DiagnosticDescriptors description: "The fields annotated with [Reactive] or [ObservableAsProperty] must be part of a class that inherits from ReactiveObject.", helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); - /// - /// The invalid reactive object error. - /// - public static readonly DiagnosticDescriptor ReadOnlyObservableCollectionTypeRequiredError = new DiagnosticDescriptor( + /// The invalid reactive object error. + internal static readonly DiagnosticDescriptor ReadOnlyObservableCollectionTypeRequiredError = new( id: "RXUISG0019", title: "Invalid field, does not inherit ReadOnlyObservableCollection", messageFormat: "The field {0}.{1} cannot be used to generate an ReadOnlyObservableCollection", diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/SuppressionDescriptors.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/SuppressionDescriptors.cs index d424d9c7..58cfaee7 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/SuppressionDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/SuppressionDescriptors.cs @@ -1,35 +1,40 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; namespace ReactiveUI.SourceGenerators.Diagnostics; +/// Contains descriptors for diagnostics that are intentionally suppressed by this generator. internal static class SuppressionDescriptors { - public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForReactiveCommandMethod = new( + /// Suppresses invalid field or property targets on generated reactive commands. + internal static readonly SuppressionDescriptor FieldOrPropertyAttributeListForReactiveCommandMethod = new( id: "RXUISPR0001", suppressedDiagnosticId: "CS0657", justification: "Methods using [ReactiveCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties"); - public static readonly SuppressionDescriptor FieldIsUsedToGenerateAObservableAsPropertyHelper = new( + /// Suppresses unused-field diagnostics for observable-as-property helpers. + internal static readonly SuppressionDescriptor FieldIsUsedToGenerateAObservableAsPropertyHelper = new( id: "RXUISPR0002", suppressedDiagnosticId: "IDE0052", justification: "Fields using [ObservableAsProperty] are never read"); - public static readonly SuppressionDescriptor ReactiveCommandDoesNotAccessInstanceData = new( + /// Suppresses static-member recommendations for generator-backed reactive members. + internal static readonly SuppressionDescriptor ReactiveCommandDoesNotAccessInstanceData = new( id: "RXUISPR0003", suppressedDiagnosticId: "CA1822", justification: "Methods using [ReactiveCommand] or [ObservableAsProperty] do not need to be static"); - public static readonly SuppressionDescriptor ReactiveFieldsShouldNotBeReadOnly = new( + /// Suppresses readonly-field recommendations for reactive fields. + internal static readonly SuppressionDescriptor ReactiveFieldsShouldNotBeReadOnly = new( id: "RXUISPR0004", suppressedDiagnosticId: "RCS1169", justification: "Fields using [Reactive] do not need to be ReadOnly"); - public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForReactiveProperty = new( + /// Suppresses invalid field or property targets on generated reactive properties. + internal static readonly SuppressionDescriptor FieldOrPropertyAttributeListForReactiveProperty = new( id: "RXUISPR0005", suppressedDiagnosticId: "CS0657", justification: "Fields using [Reactive] can use [property:] attribute lists to forward attributes to the generated properties"); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/OAPHMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/OAPHMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs index 8e197afd..efe9e975 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/OAPHMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/OAPHMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs @@ -1,11 +1,9 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using ReactiveUI.SourceGenerators.Extensions; @@ -14,15 +12,13 @@ namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; -/// -/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. -/// +/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class OAPHMethodDoesNotNeedToBeStaticDiagnosticSuppressor : DiagnosticSuppressor { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(ReactiveCommandDoesNotAccessInstanceData); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Empty.Add(ReactiveCommandDoesNotAccessInstanceData); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -30,39 +26,17 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) foreach (var diagnostic in context.ReportedDiagnostics) { var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); - - // Check that the target is a method declaration, which is the case we're looking for - if (syntaxNode is MethodDeclarationSyntax methodDeclaration) + if (syntaxNode is not (MethodDeclarationSyntax or PropertyDeclarationSyntax)) { - var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); - - // Get the method symbol from the first variable declaration - ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken); - - // Check if the method is using [ObservableAsProperty], in which case we should suppress the warning - if (declaredSymbol is IMethodSymbol methodSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol oaphSymbol && - methodSymbol.HasAttributeWithType(oaphSymbol)) - { - context.ReportSuppression(Suppression.Create(ReactiveCommandDoesNotAccessInstanceData, diagnostic)); - } + continue; } - // Check that the target is a property declaration, which is the case we're looking for - if (syntaxNode is PropertyDeclarationSyntax propertyDeclaration) + var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); + var declaredSymbol = semanticModel.GetDeclaredSymbol(syntaxNode, context.CancellationToken); + var oaphSymbol = semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType); + if (oaphSymbol is not null && declaredSymbol?.HasAttributeWithType(oaphSymbol) == true) { - var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); - - // Get the method symbol from the first variable declaration - ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, context.CancellationToken); - - // Check if the method is using [ObservableAsProperty], in which case we should suppress the warning - if (declaredSymbol is IPropertySymbol propertySymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol oaphSymbol && - propertySymbol.HasAttributeWithType(oaphSymbol)) - { - context.ReportSuppression(Suppression.Create(ReactiveCommandDoesNotAccessInstanceData, diagnostic)); - } + context.ReportSuppression(Suppression.Create(ReactiveCommandDoesNotAccessInstanceData, diagnostic)); } } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs index 5ef1e598..77dcd1a4 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; @@ -12,17 +11,15 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// ObservableAsProperty Attribute With Field Never Read Diagnostic Suppressor. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// ObservableAsProperty Attribute With Field Never Read Diagnostic Suppressor. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor : DiagnosticSuppressor - { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Empty.Add(FieldIsUsedToGenerateAObservableAsPropertyHelper); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -32,7 +29,11 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); // Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for - if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) }) + if (syntaxNode is AttributeTargetSpecifierSyntax + { + Parent.Parent: MethodDeclarationSyntax methodDeclaration, + Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword), + }) { var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); @@ -40,14 +41,13 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken); // Check if the method is using [ObservableAsProperty], in which case we should suppress the warning - if (declaredSymbol is IMethodSymbol methodSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol reactiveCommandSymbol && - methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) + if (declaredSymbol is IMethodSymbol methodSymbol + && semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol reactiveCommandSymbol + && methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) { context.ReportSuppression(Suppression.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper, diagnostic)); } } } } - } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithFieldTargetDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithFieldTargetDiagnosticSuppressor.cs index 2ea83aa6..9f35e191 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithFieldTargetDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithFieldTargetDiagnosticSuppressor.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; @@ -12,17 +11,15 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ReactiveAttributeWithFieldTargetDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class ReactiveAttributeWithFieldTargetDiagnosticSuppressor : DiagnosticSuppressor - { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveProperty); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Empty.Add(FieldOrPropertyAttributeListForReactiveProperty); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -40,14 +37,13 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, context.CancellationToken); // Check if the method is using [Reactive], in which case we should suppress the warning - if (declaredSymbol is IPropertySymbol propertySymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is INamedTypeSymbol reactiveSymbol && - propertySymbol.HasAttributeWithType(reactiveSymbol)) + if (declaredSymbol is IPropertySymbol propertySymbol + && semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is INamedTypeSymbol reactiveSymbol + && propertySymbol.HasAttributeWithType(reactiveSymbol)) { context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveProperty, diagnostic)); } } } } - } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithPropertyTargetDiagnosticSuppressor.cs index 38b06e98..8651f1a2 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithPropertyTargetDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveAttributeWithPropertyTargetDiagnosticSuppressor.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; @@ -12,17 +11,15 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ReactiveAttributeWithPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class ReactiveAttributeWithPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor - { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveProperty); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Empty.Add(FieldOrPropertyAttributeListForReactiveProperty); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -44,5 +41,4 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) } } } - } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs index 8c29b7e3..870defff 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; @@ -12,17 +11,15 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor - { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveCommandMethod); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Empty.Add(FieldOrPropertyAttributeListForReactiveCommandMethod); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -32,7 +29,11 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); // Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for - if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) }) + if (syntaxNode is AttributeTargetSpecifierSyntax + { + Parent.Parent: MethodDeclarationSyntax methodDeclaration, + Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword), + }) { var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); @@ -40,14 +41,13 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken); // Check if the method is using [ReactiveCommand], in which case we should suppress the warning - if (declaredSymbol is IMethodSymbol methodSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol && - methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) + if (declaredSymbol is IMethodSymbol methodSymbol + && semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol + && methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) { context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveCommandMethod, diagnostic)); } } } } - } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs index 8c7756ec..7d35aafc 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; @@ -14,15 +13,13 @@ namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; -/// -/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. -/// +/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor : DiagnosticSuppressor { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(ReactiveCommandDoesNotAccessInstanceData); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Empty.Add(ReactiveCommandDoesNotAccessInstanceData); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -40,9 +37,9 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken); // Check if the method is using [ReactiveCommand], in which case we should suppress the warning - if (declaredSymbol is IMethodSymbol methodSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol && - methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) + if (declaredSymbol is IMethodSymbol methodSymbol + && semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol + && methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) { context.ReportSuppression(Suppression.Create(ReactiveCommandDoesNotAccessInstanceData, diagnostic)); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs index fce32a4a..8393fcab 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; @@ -12,17 +11,15 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// Reactive Attribute ReadOnly Field Target Diagnostic Suppressor. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// Reactive Attribute ReadOnly Field Target Diagnostic Suppressor. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor : DiagnosticSuppressor - { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(ReactiveFieldsShouldNotBeReadOnly); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Empty.Add(ReactiveFieldsShouldNotBeReadOnly); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -40,14 +37,13 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) var declaredSymbol = semanticModel.GetDeclaredSymbol(fieldDeclaration, context.CancellationToken); // Check if the method is using [Reactive], in which case we should suppress the warning - if (declaredSymbol is IFieldSymbol fieldSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is INamedTypeSymbol reactiveSymbol && - fieldSymbol.HasAttributeWithType(reactiveSymbol)) + if (declaredSymbol is IFieldSymbol fieldSymbol + && semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is INamedTypeSymbol reactiveSymbol + && fieldSymbol.HasAttributeWithType(reactiveSymbol)) { context.ReportSuppression(Suppression.Create(ReactiveFieldsShouldNotBeReadOnly, diagnostic)); } } } } - } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs index 280ec471..e484cd17 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs @@ -1,10 +1,9 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; @@ -16,15 +15,26 @@ namespace ReactiveUI.SourceGenerators; -/// -/// IViewForGenerator. -/// +/// Contains implementation details for the source generator. /// public partial class IViewForGenerator { - internal static readonly string GeneratorName = typeof(IViewForGenerator).FullName!; + /// Gets the assembly version used in generated-code metadata. internal static readonly string GeneratorVersion = typeof(IViewForGenerator).Assembly.GetName().Version.ToString(); + /// Gets the fully qualified name used in generated-code metadata. + internal static readonly string GeneratorName = typeof(IViewForGenerator).FullName!; + + /// The attribute value that selects constant registration. + private const int RegisterConstantValue = 2; + + /// The attribute value that selects transient registration. + private const int RegisterValue = 3; + + /// Creates the generation model for an annotated class declaration. + /// The Roslyn context for the annotated declaration. + /// The cancellation token. + /// The generation model, or when the target is unsupported. private static IViewForInfo? GetClassInfo(in GenericGeneratorAttributeSyntaxContext context, CancellationToken token) { if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) @@ -48,7 +58,9 @@ public partial class IViewForGenerator token.ThrowIfCancellationRequested(); - var constructorArgument = attributeData.GetConstructorArguments().FirstOrDefault(); + using var constructorArguments = attributeData.GetConstructorArguments().GetEnumerator(); + var constructorArgument = constructorArguments.MoveNext() ? constructorArguments.Current : null; + var genericArgument = attributeData.GetGenericType(); token.ThrowIfCancellationRequested(); var viewModelTypeName = string.IsNullOrWhiteSpace(constructorArgument) ? genericArgument : constructorArgument; @@ -59,31 +71,7 @@ public partial class IViewForGenerator token.ThrowIfCancellationRequested(); - var viewForBaseType = IViewForBaseType.None; - if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("System.Windows.Forms")) - { - viewForBaseType = IViewForBaseType.WinForms; - } - else if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("System.Windows") || classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("System.Windows.Controls")) - { - viewForBaseType = IViewForBaseType.Wpf; - } - else if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Microsoft.UI.Xaml") || classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Microsoft.UI.Xaml.Controls")) - { - viewForBaseType = IViewForBaseType.WinUI; - } - else if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Microsoft.Maui")) - { - viewForBaseType = IViewForBaseType.Maui; - } - else if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Avalonia")) - { - viewForBaseType = IViewForBaseType.Avalonia; - } - else if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Windows.UI.Xaml") || classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Windows.UI.Xaml.Controls")) - { - viewForBaseType = IViewForBaseType.Uno; - } + var viewForBaseType = GetBaseType(classSymbol); // Get the containing type info var targetInfo = TargetInfo.From(classSymbol); @@ -91,26 +79,14 @@ public partial class IViewForGenerator token.ThrowIfCancellationRequested(); // Get RegistrationType enum value from the attribute - attributeData.TryGetNamedArgument("RegistrationType", out int splatRegistrationType); - var registrationType = splatRegistrationType switch - { - 1 => "RegisterLazySingleton", - 2 => "RegisterConstant", - 3 => "Register", - _ => string.Empty, - }; + _ = attributeData.TryGetNamedArgument("RegistrationType", out int splatRegistrationType); + var registrationType = GetRegistrationType(splatRegistrationType); token.ThrowIfCancellationRequested(); // Get ViewModelRegistrationType enum value from the attribute - attributeData.TryGetNamedArgument("ViewModelRegistrationType", out int splatViewModelRegistrationType); - var viewModelRegistrationType = splatViewModelRegistrationType switch - { - 1 => "RegisterLazySingleton", - 2 => "RegisterConstant", - 3 => "Register", - _ => string.Empty, - }; + _ = attributeData.TryGetNamedArgument("ViewModelRegistrationType", out int splatViewModelRegistrationType); + var viewModelRegistrationType = GetRegistrationType(splatViewModelRegistrationType); return new( targetInfo, @@ -120,92 +96,165 @@ public partial class IViewForGenerator viewModelRegistrationType); } - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, IViewForInfo iviewForInfo, Models.TargetInfo? parentInfo = null) + /// Identifies the supported UI framework represented by a class symbol. + /// The class symbol to inspect. + /// The matching supported base type, or . + private static IViewForBaseType GetBaseType(INamedTypeSymbol classSymbol) { - // Prepare any forwarded property attributes - var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage); + if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("System.Windows.Forms")) + { + return IViewForBaseType.WinForms; + } + + if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("System.Windows") + || classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("System.Windows.Controls")) + { + return IViewForBaseType.Wpf; + } + + if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Microsoft.UI.Xaml") + || classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Microsoft.UI.Xaml.Controls")) + { + return IViewForBaseType.WinUI; + } - // Build parent class wrapping (for nested types) + if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Microsoft.Maui")) + { + return IViewForBaseType.Maui; + } + + if (classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Avalonia")) + { + return IViewForBaseType.Avalonia; + } + + return classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Windows.UI.Xaml") + || classSymbol.InheritsFromFullyQualifiedMetadataNameStartingWith("Windows.UI.Xaml.Controls") + ? IViewForBaseType.Uno + : IViewForBaseType.None; + } + + /// Maps an attribute registration value to its Splat registration method name. + /// The integer value supplied by the attribute. + /// The Splat registration method name, or an empty string for no registration. + private static string GetRegistrationType(int registrationType) => registrationType switch + { + 1 => "RegisterLazySingleton", + RegisterConstantValue => "RegisterConstant", + RegisterValue => "Register", + _ => string.Empty, + }; + + /// Generates the partial type source for a supported IViewFor target. + /// The generation model. + /// The enclosing type model, when the target is nested. + /// The generated source, or an empty string for unsupported types. + private static string GenerateSource( + IViewForInfo viewForInfo, + TargetInfo? parentInfo = null) + { + var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage); var (parentDeclarations, parentClosing) = parentInfo is null ? (string.Empty, string.Empty) : Models.TargetInfo.GenerateParentClassDeclarations([parentInfo]); + return viewForInfo.BaseType switch + { + IViewForBaseType.Wpf or IViewForBaseType.WinUI or IViewForBaseType.Uno => GenerateDependencyPropertyViewSource(viewForInfo, parentDeclarations, parentClosing, forwardedAttributesString), + IViewForBaseType.WinForms => GenerateWinFormsViewSource(viewForInfo, parentDeclarations, parentClosing, forwardedAttributesString), + IViewForBaseType.Avalonia => GenerateAvaloniaViewSource(viewForInfo, parentDeclarations, parentClosing, forwardedAttributesString), + IViewForBaseType.Maui => GenerateMauiViewSource(viewForInfo, parentDeclarations, parentClosing, forwardedAttributesString), + _ => string.Empty, + }; + } - switch (iviewForInfo.BaseType) + /// Generates source for WPF, WinUI, and Uno views. + /// The IViewFor generation model. + /// The enclosing type declarations. + /// The enclosing type closures. + /// The forwarded attributes. + /// The generated source. + private static string GenerateDependencyPropertyViewSource(IViewForInfo info, string parents, string closingParents, string attributes) + { + var usings = info.BaseType switch { - case IViewForBaseType.None: - break; - case IViewForBaseType.Wpf: - case IViewForBaseType.WinUI: - case IViewForBaseType.Uno: - var usings = iviewForInfo.BaseType switch - { - IViewForBaseType.Wpf => """ - using ReactiveUI; - using System.Windows; - """, - IViewForBaseType.WinUI => """ - using ReactiveUI; - using Microsoft.UI.Xaml; - """, - IViewForBaseType.Uno => """ - using ReactiveUI; - using Windows.UI.Xaml; - """, - _ => string.Empty, - }; - return -$$""" + IViewForBaseType.Wpf => "using ReactiveUI;\nusing System.Windows;", + IViewForBaseType.WinUI => "using ReactiveUI;\nusing Microsoft.UI.Xaml;", + IViewForBaseType.Uno => "using ReactiveUI;\nusing Windows.UI.Xaml;", + _ => string.Empty, + }; + var viewModelPropertyDeclaration = GetDependencyPropertyDeclaration(info); + + return $$""" // {{usings}} #pragma warning disable #nullable enable -namespace {{containingNamespace}} +namespace {{info.TargetInfo.TargetNamespace}} { -{{parentDeclarations}} /// - /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. +{{parents}} /// + /// Partial class for the {{info.TargetInfo.TargetName}} which contains ReactiveUI IViewFor initialization. /// - {{forwardedAttributesString}} - {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> + {{attributes}} + {{info.TargetInfo.TargetVisibility}} partial {{info.TargetInfo.TargetType}} {{info.TargetInfo.TargetName}} : IViewFor<{{info.ViewModelTypeName}}> { /// /// The view model dependency property. /// [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] - public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof({{iviewForInfo.ViewModelTypeName}}), typeof({{containingTypeName}}), new PropertyMetadata(null)); +{{viewModelPropertyDeclaration}} /// /// Gets the binding root view model. /// - public {{iviewForInfo.ViewModelTypeName}} BindingRoot => ViewModel; + public {{info.ViewModelTypeName}} BindingRoot => ViewModel; /// - public {{iviewForInfo.ViewModelTypeName}} ViewModel { get => ({{iviewForInfo.ViewModelTypeName}})GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } + public {{info.ViewModelTypeName}} ViewModel { get => ({{info.ViewModelTypeName}})GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } /// - object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = ({{iviewForInfo.ViewModelTypeName}})value; } + object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = ({{info.ViewModelTypeName}})value; } } -{{parentClosing}}} +{{closingParents}}} #nullable restore #pragma warning restore """; - case IViewForBaseType.WinForms: - return -$$""" + } + + /// Creates the dependency-property declaration while preserving its generated one-line format. + /// The IViewFor generation model. + /// The generated dependency-property declaration. + private static string GetDependencyPropertyDeclaration(IViewForInfo info) + { + var builder = new StringBuilder(" public static readonly DependencyProperty ViewModelProperty = "); + _ = builder.Append("DependencyProperty.Register(nameof(ViewModel), typeof("); + _ = builder.Append(info.ViewModelTypeName).Append("), typeof("); + _ = builder.Append(info.TargetInfo.TargetName).Append("), new PropertyMetadata(null));"); + return builder.ToString(); + } + + /// Generates source for Windows Forms views. + /// The IViewFor generation model. + /// The enclosing type declarations. + /// The enclosing type closures. + /// The forwarded attributes. + /// The generated source. + private static string GenerateWinFormsViewSource(IViewForInfo info, string parents, string closingParents, string attributes) => + $$""" // using ReactiveUI; using System.ComponentModel; #nullable restore #pragma warning disable -namespace {{containingNamespace}} +namespace {{info.TargetInfo.TargetNamespace}} { -{{parentDeclarations}} /// - /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. +{{parents}} /// + /// Partial class for the {{info.TargetInfo.TargetName}} which contains ReactiveUI IViewFor initialization. /// - {{forwardedAttributesString}} - {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> + {{attributes}} + {{info.TargetInfo.TargetVisibility}} partial {{info.TargetInfo.TargetType}} {{info.TargetInfo.TargetName}} : IViewFor<{{info.ViewModelTypeName}}> { /// [Category("ReactiveUI")] @@ -213,18 +262,24 @@ namespace {{containingNamespace}} [Bindable(true)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] - public {{iviewForInfo.ViewModelTypeName}}? ViewModel {get; set; } + public {{info.ViewModelTypeName}}? ViewModel {get; set; } /// - object? IViewFor.ViewModel {get => ViewModel; set => ViewModel = ({{iviewForInfo.ViewModelTypeName}}? )value; } + object? IViewFor.ViewModel {get => ViewModel; set => ViewModel = ({{info.ViewModelTypeName}}? )value; } } -{{parentClosing}}} +{{closingParents}}} #nullable restore #pragma warning restore """; - case IViewForBaseType.Avalonia: - return -$$""" + + /// Generates source for Avalonia views. + /// The IViewFor generation model. + /// The enclosing type declarations. + /// The enclosing type closures. + /// The forwarded attributes. + /// The generated source. + private static string GenerateAvaloniaViewSource(IViewForInfo info, string parents, string closingParents, string attributes) => + $$""" // using System; using ReactiveUI; @@ -233,30 +288,30 @@ namespace {{containingNamespace}} #nullable restore #pragma warning disable -namespace {{containingNamespace}} +namespace {{info.TargetInfo.TargetNamespace}} { -{{parentDeclarations}} /// - /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. +{{parents}} /// + /// Partial class for the {{info.TargetInfo.TargetName}} which contains ReactiveUI IViewFor initialization. /// - {{forwardedAttributesString}} - {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> + {{attributes}} + {{info.TargetInfo.TargetVisibility}} partial {{info.TargetInfo.TargetType}} {{info.TargetInfo.TargetName}} : IViewFor<{{info.ViewModelTypeName}}> { /// /// The view model dependency property. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] - public static readonly StyledProperty<{{iviewForInfo.ViewModelTypeName}}?> ViewModelProperty = AvaloniaProperty.Register<{{containingTypeName}}, {{iviewForInfo.ViewModelTypeName}}>(nameof(ViewModel)); + public static readonly StyledProperty<{{info.ViewModelTypeName}}?> ViewModelProperty = AvaloniaProperty.Register<{{info.TargetInfo.TargetName}}, {{info.ViewModelTypeName}}>(nameof(ViewModel)); /// /// Gets the binding root view model. /// - public {{iviewForInfo.ViewModelTypeName}}? BindingRoot => ViewModel; + public {{info.ViewModelTypeName}}? BindingRoot => ViewModel; /// - public {{iviewForInfo.ViewModelTypeName}}? ViewModel { get => ({{iviewForInfo.ViewModelTypeName}}?)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } + public {{info.ViewModelTypeName}}? ViewModel { get => ({{info.ViewModelTypeName}}?)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } /// - object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = ({{iviewForInfo.ViewModelTypeName}}?)value; } + object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = ({{info.ViewModelTypeName}}?)value; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { @@ -264,7 +319,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == DataContextProperty) { - if (ReferenceEquals(change.OldValue, ViewModel) && change.NewValue is null or {{iviewForInfo.ViewModelTypeName}}) + if (ReferenceEquals(change.OldValue, ViewModel) && change.NewValue is null or {{info.ViewModelTypeName}}) { SetCurrentValue(ViewModelProperty, change.NewValue); } @@ -278,13 +333,21 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } } -{{parentClosing}}} +{{closingParents}}} #nullable restore #pragma warning restore """; - case IViewForBaseType.Maui: - return -$$""" + + /// Generates source for MAUI views. + /// The IViewFor generation model. + /// The enclosing type declarations. + /// The enclosing type closures. + /// The forwarded attributes. + /// The generated source. + private static string GenerateMauiViewSource(IViewForInfo info, string parents, string closingParents, string attributes) + { + var viewModelPropertyDeclaration = GetMauiViewModelPropertyDeclaration(info); + return $$""" // using System; using ReactiveUI; @@ -292,119 +355,62 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang #nullable restore #pragma warning disable -namespace {{containingNamespace}} +namespace {{info.TargetInfo.TargetNamespace}} { -{{parentDeclarations}} {{forwardedAttributesString}} - {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> +{{parents}} {{attributes}} + {{info.TargetInfo.TargetVisibility}} partial {{info.TargetInfo.TargetType}} {{info.TargetInfo.TargetName}} : IViewFor<{{info.ViewModelTypeName}}> { - public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(nameof(ViewModel), typeof({{iviewForInfo.ViewModelTypeName}}), typeof(IViewFor<{{iviewForInfo.ViewModelTypeName}}>), default({{iviewForInfo.ViewModelTypeName}}), BindingMode.OneWay, propertyChanged: OnViewModelChanged); +{{viewModelPropertyDeclaration}} /// /// Gets the binding root view model. /// - public {{iviewForInfo.ViewModelTypeName}}? BindingRoot => ViewModel; + public {{info.ViewModelTypeName}}? BindingRoot => ViewModel; /// - public {{iviewForInfo.ViewModelTypeName}}? ViewModel { get => ({{iviewForInfo.ViewModelTypeName}}?)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } + public {{info.ViewModelTypeName}}? ViewModel { get => ({{info.ViewModelTypeName}}?)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } /// - object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = ({{iviewForInfo.ViewModelTypeName}}?)value; } + object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = ({{info.ViewModelTypeName}}?)value; } /// protected override void OnBindingContextChanged() { base.OnBindingContextChanged(); - ViewModel = BindingContext as {{iviewForInfo.ViewModelTypeName}}; + ViewModel = BindingContext as {{info.ViewModelTypeName}}; } private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue) => bindableObject.BindingContext = newValue; } -{{parentClosing}}} +{{closingParents}}} #nullable restore #pragma warning restore """; - } - - return string.Empty; } - private static string GenerateRegistrationExtensions(in ImmutableArray iviewForInfo) + /// Creates the MAUI bindable-property declaration while preserving its generated one-line format. + /// The IViewFor generation model. + /// The generated bindable-property declaration. + private static string GetMauiViewModelPropertyDeclaration(IViewForInfo info) { - // Collapse to unique registrations and skip entries with no registration - var registrations = iviewForInfo - .Where(static x => !string.IsNullOrWhiteSpace(x.SplatRegistrationType)) - .GroupBy(static x => (x.TargetInfo.TargetNamespaceWithNamespace, x.ViewModelTypeName, x.SplatRegistrationType)) - .Select(static g => g.First()) - .ToImmutableArray(); - - var viewModelRegistrations = iviewForInfo - .Where(static x => !string.IsNullOrWhiteSpace(x.SplatViewModelRegistrationType)) - .GroupBy(static x => (x.TargetInfo.TargetNamespaceWithNamespace, x.ViewModelTypeName, x.SplatViewModelRegistrationType)) - .Select(static g => g.First()) - .ToImmutableArray(); + var builder = new StringBuilder(" public static readonly BindableProperty ViewModelProperty = "); + _ = builder.Append("BindableProperty.Create(nameof(ViewModel), typeof("); + _ = builder.Append(info.ViewModelTypeName).Append("), typeof(IViewFor<"); + _ = builder.Append(info.ViewModelTypeName).Append(">), default("); + _ = builder.Append(info.ViewModelTypeName); + _ = builder.Append("), BindingMode.OneWay, propertyChanged: OnViewModelChanged);"); + return builder.ToString(); + } + /// Generates the Splat registration extension source for discovered views. + /// The discovered view-generation models. + /// The generated extension source. + private static string GenerateRegistrationExtensions(in ImmutableArray viewForInfo) + { var sb = new StringBuilder(); - sb.AppendLine("if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));"); - foreach (var item in registrations) - { - var vmType = item.ViewModelTypeName; - if (string.IsNullOrWhiteSpace(vmType)) - { - // Something's wrong, skip it - continue; - } - - if (!vmType.StartsWith("global::", System.StringComparison.Ordinal)) - { - vmType = "global::" + vmType; - } - - var serviceType = "global::ReactiveUI.IViewFor<" + vmType + ">"; - var viewType = item.TargetInfo.TargetNamespaceWithNamespace; // already fully-qualified - - // resolver.Register*/, View>(); - switch (item.SplatRegistrationType) - { - case "RegisterLazySingleton": - sb.AppendLine($" resolver.{item.SplatRegistrationType}<{serviceType}>(() => new {viewType}());"); - break; - case "Register": - sb.AppendLine($" resolver.{item.SplatRegistrationType}<{serviceType}, {viewType}>();"); - break; - case "RegisterConstant": - sb.AppendLine($" resolver.{item.SplatRegistrationType}<{serviceType}>(new {viewType}());"); - break; - } - } - - foreach (var item in viewModelRegistrations) - { - var vmType = item.ViewModelTypeName; - if (string.IsNullOrWhiteSpace(vmType)) - { - // Something's wrong, skip it - continue; - } - - if (!vmType.StartsWith("global::", System.StringComparison.Ordinal)) - { - vmType = "global::" + vmType; - } - - // resolver.Register*/(); - switch (item.SplatViewModelRegistrationType) - { - case "RegisterLazySingleton": - sb.AppendLine($" resolver.{item.SplatViewModelRegistrationType}<{vmType}>(() => new {vmType}());"); - break; - case "Register": - sb.AppendLine($" resolver.{item.SplatViewModelRegistrationType}<{vmType}, {vmType}>();"); - break; - case "RegisterConstant": - sb.AppendLine($" resolver.{item.SplatViewModelRegistrationType}<{vmType}>(new {vmType}());"); - break; - } - } + _ = sb.AppendLine("if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));"); + AppendViewRegistrations(sb, viewForInfo); + AppendViewModelRegistrations(sb, viewForInfo); var registrationsBody = sb.ToString().TrimEnd(); return @@ -434,4 +440,108 @@ public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat. #pragma warning restore """; } + + /// Appends unique view registrations to a generated method body. + /// The generated method-body builder. + /// The discovered view-generation models. + private static void AppendViewRegistrations(StringBuilder builder, ImmutableArray viewForInfo) + { + var registrations = new HashSet<(string ViewType, string ViewModelType, string RegistrationType)>(); + foreach (var item in viewForInfo) + { + var registrationType = item.SplatRegistrationType; + var viewModelType = GetGlobalTypeName(item.ViewModelTypeName); + if (string.IsNullOrWhiteSpace(registrationType) || viewModelType is null) + { + continue; + } + + var viewType = item.TargetInfo.TargetNamespaceWithNamespace; + if (registrations.Add((viewType, viewModelType, registrationType))) + { + AppendViewRegistration(builder, registrationType, viewType, viewModelType); + } + } + } + + /// Appends unique view model registrations to a generated method body. + /// The generated method-body builder. + /// The discovered view-generation models. + private static void AppendViewModelRegistrations(StringBuilder builder, ImmutableArray viewForInfo) + { + var registrations = new HashSet<(string ViewModelType, string RegistrationType)>(); + foreach (var item in viewForInfo) + { + var registrationType = item.SplatViewModelRegistrationType; + var viewModelType = GetGlobalTypeName(item.ViewModelTypeName); + if (string.IsNullOrWhiteSpace(registrationType) || viewModelType is null) + { + continue; + } + + if (registrations.Add((viewModelType, registrationType))) + { + AppendViewModelRegistration(builder, registrationType, viewModelType); + } + } + } + + /// Normalizes a type name for use in generated source. + /// The type name supplied by the attribute. + /// A global-qualified type name, or when no type was supplied. + private static string? GetGlobalTypeName(string typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + { + return null; + } + + return typeName.StartsWith("global::", System.StringComparison.Ordinal) + ? typeName + : $"global::{typeName}"; + } + + /// Appends one view registration statement. + /// The generated method-body builder. + /// The Splat registration method. + /// The generated view type. + /// The generated view model type. + private static void AppendViewRegistration(StringBuilder builder, string registrationType, string viewType, string viewModelType) + { + var serviceType = $"global::ReactiveUI.IViewFor<{viewModelType}>"; + var registration = registrationType switch + { + "RegisterLazySingleton" => $"resolver.{registrationType}<{serviceType}>(() => new {viewType}());", + "Register" => $"resolver.{registrationType}<{serviceType}, {viewType}>();", + "RegisterConstant" => $"resolver.{registrationType}<{serviceType}>(new {viewType}());", + _ => null, + }; + if (registration is null) + { + return; + } + + _ = builder.Append(" ").AppendLine(registration); + } + + /// Appends one view model registration statement. + /// The generated method-body builder. + /// The Splat registration method. + /// The generated view model type. + private static void AppendViewModelRegistration(StringBuilder builder, string registrationType, string viewModelType) + { + var registration = registrationType switch + { + "RegisterLazySingleton" => $"resolver.{registrationType}<{viewModelType}>(() => new {viewModelType}());", + "Register" => $"resolver.{registrationType}<{viewModelType}, {viewModelType}>();", + "RegisterConstant" => $"resolver.{registrationType}<{viewModelType}>(new {viewModelType}());", + _ => null, + }; + if (registration is null) + { + return; + } + + _ = builder.Append(" ").AppendLine(registration); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs index ea967455..05c9b292 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs @@ -1,55 +1,50 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. [Generator(LanguageNames.CSharp)] public sealed partial class IViewForGenerator : IIncrementalGenerator { /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => - ctx.AddSource(AttributeDefinitions.IViewForAttributeType + ".g.cs", SourceText.From(AttributeDefinitions.IViewForAttribute, Encoding.UTF8))); + context.RegisterPostInitializationOutput(static ctx => + ctx.AddSource($"{AttributeDefinitions.IViewForAttributeType}.g.cs", SourceText.From(AttributeDefinitions.IViewForAttribute, Encoding.UTF8))); // Gather info for all annotated IViewFor Classes - var iViewForInfo = + var viewForInfo = context.SyntaxProvider .ForAttributeWithMetadataNameWithGenerics( AttributeDefinitions.IViewForAttributeType, static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, static (context, token) => GetClassInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) + .Where(static x => x is not null) + .Select(static (x, _) => x!) .Collect(); // Generate the requested properties and methods for IViewFor - context.RegisterSourceOutput(iViewForInfo, static (context, input) => + context.RegisterSourceOutput(viewForInfo, static (context, input) => { - var groupedPropertyInfo = input.GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); + var groupedPropertyInfo = GroupByTarget(input); const string fileName = "ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.cs"; - if (groupedPropertyInfo.Length == 0) + if (groupedPropertyInfo.Count == 0) { // Even if there are no views, emit an empty extension to keep API stable. - var empty = GenerateRegistrationExtensions(ImmutableArray.Create()); + var empty = GenerateRegistrationExtensions(ImmutableArray.Empty); context.AddSource(fileName, SourceText.From(empty, Encoding.UTF8)); return; } @@ -58,23 +53,40 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var registrationSource = GenerateRegistrationExtensions(input); context.AddSource(fileName, SourceText.From(registrationSource, Encoding.UTF8)); - foreach (var grouping in groupedPropertyInfo) + foreach (var grouping in groupedPropertyInfo.Values) { - var items = grouping.ToImmutableArray(); - - if (items.Length == 0) - { - continue; - } - - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault(), grouping.FirstOrDefault()?.TargetInfo?.ParentInfo); + var info = grouping[0]; + var source = GenerateSource(info, info.TargetInfo.ParentInfo); // Only add source if it's not empty (i.e., a supported UI framework base type was detected) if (!string.IsNullOrWhiteSpace(source)) { - context.AddSource(grouping.Key.FileHintName + ".IViewFor.g.cs", source); + context.AddSource($"{info.TargetInfo.FileHintName}.IViewFor.g.cs", source); } } }); } + + /// Groups source-generation inputs by their annotated target type. + /// The discovered IViewFor targets. + /// The targets grouped by their generated file identity. + private static Dictionary<(string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), List> GroupByTarget( + ImmutableArray input) + { + Dictionary<(string, string, string, string, string), List> result = []; + foreach (var info in input) + { + var target = info.TargetInfo; + var key = (target.FileHintName, target.TargetName, target.TargetNamespace, target.TargetVisibility, target.TargetType); + if (!result.TryGetValue(key, out var values)) + { + values = []; + result.Add(key, values); + } + + values.Add(info); + } + + return result; + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForBaseType.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForBaseType.cs index e2f0d382..aa4373f7 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForBaseType.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForBaseType.cs @@ -1,17 +1,24 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerators.Models; +/// Identifies the UI framework base type implemented by an IViewFor target. internal enum IViewForBaseType { + /// No supported base type was found. None, + /// The target is a Windows Presentation Foundation control. Wpf, + /// The target is a WinUI control. WinUI, + /// The target is a Uno Platform control. Uno, + /// The target is a Windows Forms control. WinForms, + /// The target is an Avalonia control. Avalonia, + /// The target is a .NET MAUI control. Maui, } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs index 40266181..645de57b 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs @@ -1,13 +1,15 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model with gathered info on a given command method. -/// +/// Contains the information needed to generate an IViewFor implementation. +/// The annotated target type. +/// The fully qualified view model type name. +/// The supported UI framework base type. +/// The Splat view registration method to invoke. +/// The Splat view model registration method to invoke. internal sealed record IViewForInfo( TargetInfo TargetInfo, string ViewModelTypeName, diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableFieldInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableFieldInfo.cs index fb030aed..8851d062 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableFieldInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableFieldInfo.cs @@ -1,15 +1,23 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model with gathered info on a given field. -/// +/// A model with gathered info on a given field. +/// The target type that owns the generated property. +/// The property's fully qualified type name. +/// The source field name. +/// The generated property name. +/// The source field initializer, when present. +/// Whether the field type permits null. +/// Whether a member-not-null annotation is required. +/// Attributes copied to the generated property. +/// The generated property's read-only modifier. +/// The generated property's accessibility. +/// The generated property's inheritance modifier. internal sealed record ObservableFieldInfo( TargetInfo TargetInfo, string TypeNameWithNullabilityAnnotations, diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs index 365addda..8906c80c 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs @@ -1,36 +1,52 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Globalization; using ReactiveUI.SourceGenerators.Helpers; -namespace ReactiveUI.SourceGenerators.Models +namespace ReactiveUI.SourceGenerators.Models; + +/// Captures the metadata needed to generate an observable-backed property. +/// The target type that owns the generated property. +/// The source method or property name. +/// The source member's return type. +/// The source method's argument type, when present. +/// The generated property name. +/// The observable's fully qualified type name. +/// Whether the observable element type permits null. +/// Whether the source member is a property. +/// Attributes copied to the generated property. +/// The generated property's read-only modifier. +/// The generated property's accessibility. +/// The generated property's initial value, when present. +internal sealed record ObservableMethodInfo( + TargetInfo TargetInfo, + string MethodName, + string MethodReturnType, + string? ArgumentType, + string PropertyName, + string ObservableType, + bool IsNullableType, + bool IsProperty, + EquatableArray ForwardedPropertyAttributes, + string IsReadOnly, + string AccessModifier, + string? InitialValue) { - internal record ObservableMethodInfo( - TargetInfo TargetInfo, - string MethodName, - string MethodReturnType, - string? ArgumentType, - string PropertyName, - string ObservableType, - bool IsNullableType, - bool IsProperty, - EquatableArray ForwardedPropertyAttributes, - string IsReadOnly, - string AccessModifier, - string? InitialValue) - { - public bool IsFromPartialProperty => ObservableType.Contains("##FromPartialProperty##"); + /// Gets whether this model originated from a partial property. + internal bool IsFromPartialProperty => ObservableType.IndexOf("##FromPartialProperty##", StringComparison.Ordinal) >= 0; - public string PartialPropertyType => ObservableType.Replace("##FromPartialProperty##", string.Empty); + /// Gets the observable type with its partial-property marker removed. + internal string PartialPropertyType => ObservableType.Replace("##FromPartialProperty##", string.Empty); - public string GetGeneratedFieldName() - { - var propertyName = PropertyName; + /// Gets the field name generated for this property. + /// The generated backing field name. + internal string GetGeneratedFieldName() + { + var propertyName = PropertyName; - return $"_{char.ToLower(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; - } + return $"_{char.ToLower(propertyName[0], CultureInfo.InvariantCulture)}{propertyName[1..]}"; } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator.cs index f40885f7..a7597173 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Text; @@ -12,19 +11,20 @@ namespace ReactiveUI.SourceGenerators; -/// -/// Main entry point. -/// +/// Main entry point. [Generator(LanguageNames.CSharp)] public sealed partial class ObservableAsPropertyGenerator : IIncrementalGenerator { + /// Gets the generator type name used in generated-code metadata. internal static readonly string GeneratorName = typeof(ObservableAsPropertyGenerator).FullName!; + + /// Gets the generator assembly version used in generated-code metadata. internal static readonly string GeneratorVersion = typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString(); /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => ctx.AddSource($"{AttributeDefinitions.ObservableAsPropertyAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ObservableAsPropertyAttribute, Encoding.UTF8))); RunObservableAsPropertyFromObservable(context); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs index 7a7b3e6c..f7ad7b38 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs @@ -1,10 +1,9 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -15,19 +14,20 @@ namespace ReactiveUI.SourceGenerators; -/// -/// ReactiveGenerator. -/// +/// Implements ObservableAsProperty generation from annotated fields. /// public sealed partial class ObservableAsPropertyGenerator { + /// Creates metadata for an ObservableAsProperty-annotated field. + /// The attribute syntax context for the field. + /// The cancellation token for the generator operation. + /// The field metadata and diagnostics, or when not applicable. private static Result? GetVariableInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { using var builder = ImmutableArrayBuilder.Rent(); var symbol = context.TargetSymbol; token.ThrowIfCancellationRequested(); - // Skip symbols without the target attribute if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType, out var attributeData)) { return default; @@ -38,7 +38,6 @@ public sealed partial class ObservableAsPropertyGenerator return default; } - // Validate the target type if (!fieldSymbol.IsTargetTypeValid()) { builder.Add( @@ -49,32 +48,31 @@ public sealed partial class ObservableAsPropertyGenerator return new(default, builder.ToImmutable()); } - // Get the can PropertyName member, if any - attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); - - // Get Inheritance value from the attribute - attributeData.TryGetNamedArgument("Inheritance", out int? inheritanceArgument); - var inheritance = inheritanceArgument switch - { - 1 => " virtual", - 2 => " override", - 3 => " new", - _ => string.Empty, - }; - - token.ThrowIfCancellationRequested(); + return CreateResult(context, fieldSymbol, attributeData, builder, token); + } - attributeData.TryGetNamedArgument("UseProtected", out bool useProtected); - var useProtectedModifier = useProtected ? "protected" : "private"; + /// Creates metadata for a validated ObservableAsProperty field. + /// The attribute syntax context for the field. + /// The validated field symbol. + /// The ObservableAsProperty attribute data. + /// The diagnostics collected while processing the field. + /// The cancellation token for the generator operation. + /// The field metadata and diagnostics. + private static Result CreateResult( + in GeneratorAttributeSyntaxContext context, + IFieldSymbol fieldSymbol, + AttributeData attributeData, + ImmutableArrayBuilder builder, + CancellationToken token) + { + var (isReadonly, useProtectedModifier, inheritance) = GetAttributeOptions(attributeData); token.ThrowIfCancellationRequested(); - // Get the property type and name var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); var fieldName = fieldSymbol.Name; var propertyName = fieldSymbol.GetGeneratedPropertyName(); - // Check for name collisions if (fieldName == propertyName) { builder.Add( @@ -86,7 +84,8 @@ public sealed partial class ObservableAsPropertyGenerator } var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; - var initializer = fieldDeclaration.Declaration.Variables.FirstOrDefault()?.Initializer?.ToFullString(); + var variables = fieldDeclaration.Declaration.Variables; + var initializer = variables.Count > 0 ? variables[0].Initializer?.ToFullString() : null; token.ThrowIfCancellationRequested(); @@ -99,15 +98,11 @@ public sealed partial class ObservableAsPropertyGenerator token.ThrowIfCancellationRequested(); - // Get the nullability info for the property - fieldSymbol.GetNullabilityInfo( - context.SemanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); + var (isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor) = + GetNullabilityInfo(fieldSymbol, context.SemanticModel); token.ThrowIfCancellationRequested(); - // Get the containing type info var targetInfo = TargetInfo.From(fieldSymbol.ContainingType); return new( @@ -126,12 +121,63 @@ public sealed partial class ObservableAsPropertyGenerator builder.ToImmutable()); } - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ObservableFieldInfo[] properties) + /// Gets property-generation options from an ObservableAsProperty attribute. + /// The ObservableAsProperty attribute data. + /// The read-only flag, accessibility, and inheritance modifier. + private static (bool? IsReadonly, string Accessibility, string Inheritance) GetAttributeOptions(AttributeData attributeData) + { + _ = attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); + _ = attributeData.TryGetNamedArgument("Inheritance", out int? inheritanceArgument); + _ = attributeData.TryGetNamedArgument("UseProtected", out bool useProtected); + + const int OverrideInheritance = 2; + const int NewInheritance = 3; + var inheritance = inheritanceArgument switch + { + 1 => " virtual", + OverrideInheritance => " override", + NewInheritance => " new", + _ => string.Empty, + }; + + return (isReadonly, useProtected ? "protected" : "private", inheritance); + } + + /// Gets nullability metadata for a generated observable property. + /// The source field symbol. + /// The semantic model for the source field. + /// The reference-type and member-not-null metadata. + private static (bool IsReferenceType, bool IncludeMemberNotNull) GetNullabilityInfo( + IFieldSymbol fieldSymbol, + SemanticModel semanticModel) + { + fieldSymbol.GetNullabilityInfo( + semanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); + return (isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor); + } + + /// Generates the complete source document for field-backed observable properties. + /// The name of the containing type. + /// The containing namespace. + /// The containing type visibility. + /// The containing type kind. + /// The generated property metadata. + /// The selected ReactiveUI integration metadata. + /// The generated source document. + private static string GenerateSource( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + ObservableFieldInfo[] properties, + ReactiveUiIntegration integration) { // Get Parent class details from properties.ParentInfo - var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(properties.Select(p => p.TargetInfo.ParentInfo).ToArray()); + var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(GetParentInfos(properties)); - var classes = GenerateClassWithProperties(containingTypeName, containingNamespace, containingClassVisibility, containingType, properties); + var classes = GenerateClassWithProperties(containingTypeName, containingClassVisibility, containingType, properties, integration.DeclarationNamespace); return $$""" // @@ -146,19 +192,22 @@ namespace {{containingNamespace}} """; } - /// - /// Generates the source code. - /// + /// Generates the source code. /// The contain type name. - /// The containing namespace. /// The containing class visibility. /// The containing type. /// The properties. + /// The namespace containing the selected ReactiveUI implementation types. /// The value. - private static string GenerateClassWithProperties(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ObservableFieldInfo[] properties) + private static string GenerateClassWithProperties( + string containingTypeName, + string containingClassVisibility, + string containingType, + ObservableFieldInfo[] properties, + string reactiveUiNamespace) { // Includes 2 tabs from the property declarations so no need to add them here. - var propertyDeclarations = string.Join("\n", properties.Select(GetPropertySyntax)); + var propertyDeclarations = GetPropertyDeclarations(properties, reactiveUiNamespace); return $$""" @@ -170,24 +219,28 @@ private static string GenerateClassWithProperties(string containingTypeName, str """; } - private static string GetPropertySyntax(ObservableFieldInfo propertyInfo) + /// Generates a property declaration for an observable field. + /// The source field metadata. + /// The ReactiveUI implementation namespace. + /// The generated property declaration. + private static string GetPropertySyntax(ObservableFieldInfo propertyInfo, string reactiveUiNamespace) { - var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); + var propertyAttributes = GetPropertyAttributes(propertyInfo); var getter = $$"""{ get => {{propertyInfo.FieldName}} = {{propertyInfo.FieldName}}Helper?.Value ?? {{propertyInfo.FieldName}}; }"""; // If the property is nullable, we need to add a null check to the getter - if (propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?")) + if (propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?", System.StringComparison.Ordinal)) { getter = $$"""{ get => {{propertyInfo.FieldName}} = ({{propertyInfo.FieldName}}Helper == null ? {{propertyInfo.FieldName}} : {{propertyInfo.FieldName}}Helper.Value); }"""; } - var helperTypeName = $"{propertyInfo.AccessModifier} ReactiveUI.ObservableAsPropertyHelper<{propertyInfo.TypeNameWithNullabilityAnnotations}>?"; + var helperTypeName = $"{propertyInfo.AccessModifier} {reactiveUiNamespace}.ObservableAsPropertyHelper<{propertyInfo.TypeNameWithNullabilityAnnotations}>?"; // If the property is readonly, we need to change the helper to be non-nullable if (propertyInfo.IsReadOnly == "readonly") { - helperTypeName = $"{propertyInfo.AccessModifier} readonly ReactiveUI.ObservableAsPropertyHelper<{propertyInfo.TypeNameWithNullabilityAnnotations}>"; + helperTypeName = $"{propertyInfo.AccessModifier} readonly {reactiveUiNamespace}.ObservableAsPropertyHelper<{propertyInfo.TypeNameWithNullabilityAnnotations}>"; } return $$""" @@ -200,4 +253,70 @@ private static string GetPropertySyntax(ObservableFieldInfo propertyInfo) public {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} {{getter}} """; } + + /// Gets parent information for each generated property. + /// The generated property metadata. + /// The parent information for the generated properties. + private static TargetInfo?[] GetParentInfos(ObservableFieldInfo[] properties) + { + var parentInfos = new TargetInfo?[properties.Length]; + for (var index = 0; index < properties.Length; index++) + { + parentInfos[index] = properties[index].TargetInfo.ParentInfo; + } + + return parentInfos; + } + + /// Gets the generated property declarations. + /// The generated property metadata. + /// The ReactiveUI implementation namespace. + /// The generated property declarations. + private static string GetPropertyDeclarations(ObservableFieldInfo[] properties, string reactiveUiNamespace) + { + var builder = new StringBuilder(); + for (var index = 0; index < properties.Length; index++) + { + if (index > 0) + { + _ = builder.Append('\n'); + } + + _ = builder.Append(GetPropertySyntax(properties[index], reactiveUiNamespace)); + } + + return builder.ToString(); + } + + /// Gets attributes applied to a generated property. + /// The source field metadata. + /// The property attributes separated by generated-code indentation. + private static string GetPropertyAttributes(ObservableFieldInfo propertyInfo) + { + var builder = new StringBuilder(); + foreach (var attribute in AttributeDefinitions.ExcludeFromCodeCoverage) + { + AppendPropertyAttribute(builder, attribute); + } + + foreach (var attribute in propertyInfo.ForwardedAttributes.AsImmutableArray()) + { + AppendPropertyAttribute(builder, attribute); + } + + return builder.ToString(); + } + + /// Appends a generated property attribute with the required separator. + /// The destination builder. + /// The attribute source. + private static void AppendPropertyAttribute(StringBuilder builder, string attribute) + { + if (builder.Length > 0) + { + _ = builder.Append("\n "); + } + + _ = builder.Append(attribute); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs index f2c2e7c5..e2b0c1f2 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs @@ -1,67 +1,76 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. public sealed partial class ObservableAsPropertyGenerator { + /// Registers generation of observable-backed properties declared from fields. + /// The incremental generator initialization context. private static void RunObservableAsPropertyFromField(in IncrementalGeneratorInitializationContext context) { var propertyInfo = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.ObservableAsPropertyAttributeType, - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + static (node, _) => node is VariableDeclaratorSyntax + { + Parent.Parent: FieldDeclarationSyntax + { + Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, + AttributeLists.Count: > 0, + }, + }, static (context, token) => GetVariableInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) - .Collect(); + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .Collect() + .Combine(context.ReactiveUiIntegration()); // Generate the requested properties and methods context.RegisterSourceOutput(propertyInfo, static (context, input) => { - foreach (var diagnostic in input.SelectMany(static x => x.Errors)) - { - // Output the diagnostics - context.ReportDiagnostic(diagnostic.ToDiagnostic()); - } + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + List> groupedPropertyInfo = []; - // Gather all the properties that are valid and group them by the target information. - var groupedPropertyInfo = input - .Where(static x => x.Value != null) - .Select(static x => x.Value!).GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); - - if (groupedPropertyInfo.Length == 0) + foreach (var result in input.Left) { - return; - } - - foreach (var grouping in groupedPropertyInfo) - { - var items = grouping.ToImmutableArray(); + foreach (var diagnostic in result.Errors.AsImmutableArray()) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } - if (items.Length == 0) + if (result.Value is not ObservableFieldInfo propertyInfo) { continue; } - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + var targetInfo = propertyInfo.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedPropertyInfo.TryGetValue(key, out var properties)) + { + properties = []; + groupedPropertyInfo.Add(key, properties); + } + + properties.Add(propertyInfo); + } + + foreach (var grouping in groupedPropertyInfo) + { + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.Value.ToArray(), input.Right); context.AddSource($"{grouping.Key.FileHintName}.ObservableAsProperties.g.cs", source); } }); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs index 60a15b68..f540f17c 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs @@ -1,9 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Collections.Immutable; using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,12 +15,14 @@ namespace ReactiveUI.SourceGenerators; -/// -/// Observable As Property From Observable Generator. -/// +/// Observable As Property From Observable Generator. /// public sealed partial class ObservableAsPropertyGenerator { + /// Creates metadata for an ObservableAsProperty-annotated method or property. + /// The attribute syntax context for the source member. + /// The cancellation token for the generator operation. + /// The member metadata and diagnostics, or when not applicable. private static Result? GetObservableInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { using var diagnostics = ImmutableArrayBuilder.Rent(); @@ -29,236 +32,284 @@ public sealed partial class ObservableAsPropertyGenerator var attributeData = context.Attributes[0]; // Get the can PropertyName member, if any - attributeData.TryGetNamedArgument("PropertyName", out string? propertyName); + _ = attributeData.TryGetNamedArgument("PropertyName", out string? propertyName); // Get the can InitialValue member, if any - attributeData.TryGetNamedArgument("InitialValue", out string? initialValue); + _ = attributeData.TryGetNamedArgument("InitialValue", out string? initialValue); token.ThrowIfCancellationRequested(); - attributeData.TryGetNamedArgument("UseProtected", out bool useProtected); + _ = attributeData.TryGetNamedArgument("UseProtected", out bool useProtected); var useProtectedModifier = useProtected ? "protected" : "private"; token.ThrowIfCancellationRequested(); // Get the can ReadOnly member, if any - attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); + _ = attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); token.ThrowIfCancellationRequested(); - var compilation = context.SemanticModel.Compilation; - if (context.TargetNode is MethodDeclarationSyntax methodSyntax) + var settings = new ObservablePropertySettings(propertyName, initialValue, useProtectedModifier, isReadonly); + return context.TargetNode switch { - var methodSymbol = (IMethodSymbol)symbol!; - - // Validate the target type - if (!methodSymbol.IsTargetTypeValid()) - { - diagnostics.Add( - InvalidReactiveObjectError, - methodSymbol, - methodSymbol.ContainingType, - methodSymbol.Name); - return new(default, diagnostics.ToImmutable()); - } - - if (methodSymbol.Parameters.Length != 0) - { - diagnostics.Add( - ObservableAsPropertyMethodHasParametersError, - methodSymbol, - methodSymbol.Name); - return new(default, diagnostics.ToImmutable()); - } - - var isObservable = methodSymbol.ReturnType.IsObservableReturnType(); - if (!isObservable) - { - return default; - } - - token.ThrowIfCancellationRequested(); - context.GetForwardedAttributes( - diagnostics, + MethodDeclarationSyntax methodSyntax when symbol is IMethodSymbol methodSymbol => GetObservableMethodInfo( + context, + methodSyntax, methodSymbol, - methodSyntax.AttributeLists, - token, - out var propertyAttributes); - - token.ThrowIfCancellationRequested(); + propertyName, + initialValue, + useProtectedModifier, + token), + PropertyDeclarationSyntax propertySyntax when symbol is IPropertySymbol propertySymbol => GetObservablePropertyInfo( + context, + propertySyntax, + propertySymbol, + in settings, + diagnostics, + token), + _ => default, + }; + } - var observableType = methodSymbol.ReturnType is not INamedTypeSymbol typeSymbol - ? string.Empty - : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); + /// Creates metadata for an ObservableAsProperty-annotated property. + /// The attribute syntax context for the property. + /// The property declaration syntax. + /// The property symbol. + /// The settings declared by the attribute. + /// The builder that receives diagnostics. + /// The cancellation token for the generator operation. + /// The property metadata and diagnostics, or when not applicable. + private static Result? GetObservablePropertyInfo( + in GeneratorAttributeSyntaxContext context, + PropertyDeclarationSyntax propertySyntax, + IPropertySymbol propertySymbol, + in ObservablePropertySettings settings, + ImmutableArrayBuilder diagnostics, + CancellationToken token) + { + if (!propertySymbol.IsTargetTypeValid()) + { + diagnostics.Add(InvalidReactiveObjectError, propertySymbol, propertySymbol.ContainingType, propertySymbol.Name); + return new(default, diagnostics.ToImmutable()); + } - var isNullableType = methodSymbol.ReturnType is INamedTypeSymbol nullcheck && nullcheck.TypeArguments[0].IsNullableType(); + token.ThrowIfCancellationRequested(); + context.GetForwardedAttributes(diagnostics, propertySymbol, propertySyntax.AttributeLists, token, out var propertyAttributes); + token.ThrowIfCancellationRequested(); - token.ThrowIfCancellationRequested(); + return propertySymbol.Type.IsObservableReturnType() + ? CreateObservablePropertyInfo( + propertySymbol, + in settings, + propertyAttributes, + diagnostics) - // Get the containing type info - var targetInfo = TargetInfo.From(methodSymbol.ContainingType); +#if ROSYLN_412 || ROSYLN_500 + : GetPartialObservablePropertyInfo( + context, + propertySyntax, + propertySymbol, + in settings, + propertyAttributes, + diagnostics, + token); +#else + : default; +#endif + } - return new( - new( + /// Creates the metadata for a property that returns an observable. + /// The property symbol. + /// The settings declared by the attribute. + /// The attributes to forward to the generated property. + /// The builder that receives diagnostics. + /// The property metadata and diagnostics. + private static Result CreateObservablePropertyInfo( + IPropertySymbol propertySymbol, + in ObservablePropertySettings settings, + ImmutableArray propertyAttributes, + ImmutableArrayBuilder diagnostics) + { + var observableType = GetObservableElementType(propertySymbol.Type); + var isNullableType = IsObservableElementNullable(propertySymbol.Type); + var targetInfo = TargetInfo.From(propertySymbol.ContainingType); + return new( + new( targetInfo, - methodSymbol.Name, - methodSymbol.ReturnType.GetFullyQualifiedNameWithNullabilityAnnotations(), - methodSymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - propertyName ?? (methodSymbol.Name + "Property"), + propertySymbol.Name, + propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), + propertySymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + settings.PropertyName ?? $"{propertySymbol.Name}Property", observableType, isNullableType, - false, + true, propertyAttributes, string.Empty, - useProtectedModifier, - initialValue), - diagnostics.ToImmutable()); - } + settings.UseProtectedModifier, + settings.InitialValue), + diagnostics.ToImmutable()); + } - if (context.TargetNode is PropertyDeclarationSyntax propertySyntax) - { - if (symbol is not IPropertySymbol propertySymbol) - { - return default; - } + /// Gets the element type of an observable property. + /// The property type. + /// The fully qualified observable element type, or an empty string when unavailable. + private static string GetObservableElementType(ITypeSymbol propertyType) => + propertyType is INamedTypeSymbol typeSymbol + ? typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations() + : string.Empty; - // Validate the target type - if (!propertySymbol.IsTargetTypeValid()) - { - diagnostics.Add( - InvalidReactiveObjectError, - propertySymbol, - propertySymbol.ContainingType, - propertySymbol.Name); - return new(default, diagnostics.ToImmutable()); - } + /// Determines whether the element type of an observable property permits null. + /// The property type. + /// when the observable element type permits null; otherwise, . + private static bool IsObservableElementNullable(ITypeSymbol propertyType) => + propertyType is INamedTypeSymbol typeSymbol && typeSymbol.TypeArguments[0].IsNullableType(); - var observableType = string.Empty; - var isNullableType = false; - var isPartialProperty = false; +#if ROSYLN_412 || ROSYLN_500 + /// Creates metadata for a partial property that supplies its own observable value. + /// The attribute syntax context for the property. + /// The property declaration syntax. + /// The property symbol. + /// The settings declared by the attribute. + /// The attributes to forward to the generated property. + /// The builder that receives diagnostics. + /// The cancellation token for the generator operation. + /// The property metadata and diagnostics, or when not applicable. + private static Result? GetPartialObservablePropertyInfo( + in GeneratorAttributeSyntaxContext context, + PropertyDeclarationSyntax propertySyntax, + IPropertySymbol propertySymbol, + in ObservablePropertySettings settings, + ImmutableArray propertyAttributes, + ImmutableArrayBuilder diagnostics, + CancellationToken token) + { + if (!propertySymbol.IsPartialDefinition || propertySymbol.IsStatic) + { + return default; + } - token.ThrowIfCancellationRequested(); - context.GetForwardedAttributes( - diagnostics, + token.ThrowIfCancellationRequested(); + var propertyType = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + var fieldName = propertySymbol.GetGeneratedFieldName(); + var generatedPropertyName = propertySymbol.Name; + if (fieldName == generatedPropertyName) + { + diagnostics.Add( + ReactivePropertyNameCollisionError, propertySymbol, - propertySyntax.AttributeLists, - token, - out var propertyAttributes); - - token.ThrowIfCancellationRequested(); - - if (propertySymbol.Type.IsObservableReturnType()) - { - observableType = propertySymbol.Type is not INamedTypeSymbol typeSymbol - ? string.Empty - : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); + propertySymbol.ContainingType, + propertySymbol.Name); + return new(default, diagnostics.ToImmutable()); + } - token.ThrowIfCancellationRequested(); + token.ThrowIfCancellationRequested(); + context.GetForwardedAttributes(diagnostics, propertySymbol, propertySyntax.AttributeLists, token, out _); + token.ThrowIfCancellationRequested(); - isNullableType = propertySymbol.Type is INamedTypeSymbol nullcheck && nullcheck.TypeArguments[0].IsNullableType(); - } -#if ROSYLN_412 || ROSYLN_500 - else - { - if (!propertySymbol.IsPartialDefinition || propertySymbol.IsStatic) - { - return default; - } - - // Validate the target type - if (!propertySymbol.IsTargetTypeValid()) - { - diagnostics.Add( - InvalidReactiveObjectError, - propertySymbol, - propertySymbol.ContainingType, - propertySymbol.Name); - return new(default, diagnostics.ToImmutable()); - } - - token.ThrowIfCancellationRequested(); - - isPartialProperty = true; - - var inheritance = propertySymbol.IsVirtual ? " virtual" : propertySymbol.IsOverride ? " override" : string.Empty; - - token.ThrowIfCancellationRequested(); - - // Get the property type and name - var typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - - // Get the field name - var fieldName = propertySymbol.GetGeneratedFieldName(); - propertyName = propertySymbol.Name; - - // Check for names for collisions - if (fieldName == propertyName) - { - diagnostics.Add( - ReactivePropertyNameCollisionError, - propertySymbol, - propertySymbol.ContainingType, - propertySymbol.Name); - return new(default, diagnostics.ToImmutable()); - } - - var propertyDeclaration = (PropertyDeclarationSyntax)context.TargetNode!; - - token.ThrowIfCancellationRequested(); - - context.GetForwardedAttributes( - diagnostics, - propertySymbol, - propertyDeclaration.AttributeLists, - token, - out var forwardedPropertyAttributes); - - token.ThrowIfCancellationRequested(); - - observableType = "##FromPartialProperty##" + typeNameWithNullabilityAnnotations; - } + var isReadOnly = settings.IsReadOnly == false ? string.Empty : "readonly"; + var targetInfo = TargetInfo.From(propertySymbol.ContainingType); + return new( + new( + targetInfo, + propertySymbol.Name, + propertyType, + propertySymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + generatedPropertyName, + $"##FromPartialProperty##{propertyType}", + false, + true, + propertyAttributes, + isReadOnly, + settings.UseProtectedModifier, + settings.InitialValue), + diagnostics.ToImmutable()); + } #endif - var isReadOnlyString = string.Empty; - if (isPartialProperty) - { - isReadOnlyString = isReadonly == false ? string.Empty : "readonly"; - } + /// Creates metadata for an ObservableAsProperty-annotated method. + /// The attribute syntax context for the method. + /// The method declaration syntax. + /// The method symbol. + /// The optional generated property name. + /// The optional generated property initial value. + /// The generated helper accessibility. + /// The cancellation token for the generator operation. + /// The method metadata and diagnostics, or when not applicable. + private static Result? GetObservableMethodInfo( + in GeneratorAttributeSyntaxContext context, + MethodDeclarationSyntax methodSyntax, + IMethodSymbol methodSymbol, + string? propertyName, + string? initialValue, + string useProtectedModifier, + CancellationToken token) + { + using var diagnostics = ImmutableArrayBuilder.Rent(); + if (!methodSymbol.IsTargetTypeValid()) + { + diagnostics.Add(InvalidReactiveObjectError, methodSymbol, methodSymbol.ContainingType, methodSymbol.Name); + return new(default, diagnostics.ToImmutable()); + } + + if (!methodSymbol.Parameters.IsEmpty) + { + diagnostics.Add(ObservableAsPropertyMethodHasParametersError, methodSymbol, methodSymbol.Name); + return new(default, diagnostics.ToImmutable()); + } - // Get the containing type info - var targetInfo = TargetInfo.From(propertySymbol.ContainingType); + if (!methodSymbol.ReturnType.IsObservableReturnType()) + { + return default; + } - return new( - new( + context.GetForwardedAttributes(diagnostics, methodSymbol, methodSyntax.AttributeLists, token, out var propertyAttributes); + var observableType = methodSymbol.ReturnType is INamedTypeSymbol typeSymbol + ? typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations() + : string.Empty; + var isNullableType = methodSymbol.ReturnType is INamedTypeSymbol nullcheck && nullcheck.TypeArguments[0].IsNullableType(); + var targetInfo = TargetInfo.From(methodSymbol.ContainingType); + return new( + new( targetInfo, - propertySymbol.Name, - propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), - propertySymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - propertyName ?? (propertySymbol.Name + "Property"), + methodSymbol.Name, + methodSymbol.ReturnType.GetFullyQualifiedNameWithNullabilityAnnotations(), + methodSymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + propertyName ?? $"{methodSymbol.Name}Property", observableType, isNullableType, - true, + false, propertyAttributes, - isReadOnlyString, + string.Empty, useProtectedModifier, initialValue), - diagnostics.ToImmutable()); - } - - return default; + diagnostics.ToImmutable()); } - private static string GenerateObservableSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ObservableMethodInfo[] properties) + /// Generates the complete source document for method-backed observable properties. + /// The name of the containing type. + /// The containing namespace. + /// The containing type visibility. + /// The containing type kind. + /// The generated property metadata. + /// The selected ReactiveUI integration metadata. + /// The generated source document. + private static string GenerateObservableSource( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + ObservableMethodInfo[] properties, + ReactiveUiIntegration integration) { // Get Parent class details from properties.ParentInfo - var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(properties.Select(p => p.TargetInfo.ParentInfo).ToArray()); + var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(GetParentInfos(properties)); - var classes = GenerateClassWithProperties(containingTypeName, containingNamespace, containingClassVisibility, containingType, properties); + var classes = GenerateClassWithProperties(containingTypeName, containingClassVisibility, containingType, properties, integration.DeclarationNamespace); return $$""" // -using ReactiveUI; +{{integration.UsingDirectives}} #pragma warning disable #nullable enable @@ -272,19 +323,22 @@ namespace {{containingNamespace}} """; } - /// - /// Generates the source code. - /// + /// Generates the source code. /// The contain type name. - /// The containing namespace. /// The containing class visibility. /// The containing type. /// The properties. + /// The namespace containing the selected ReactiveUI implementation types. /// The value. - private static string GenerateClassWithProperties(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ObservableMethodInfo[] properties) + private static string GenerateClassWithProperties( + string containingTypeName, + string containingClassVisibility, + string containingType, + ObservableMethodInfo[] properties, + string reactiveUiNamespace) { // Includes 2 tabs from the property declarations so no need to add them here. - var propertyDeclarations = string.Join("\n", properties.Select(GetPropertySyntax)); + var propertyDeclarations = GetPropertyDeclarations(properties, reactiveUiNamespace); return $$""" @@ -298,9 +352,13 @@ private static string GenerateClassWithProperties(string containingTypeName, str """; } - private static string GetPropertySyntax(ObservableMethodInfo propertyInfo) + /// Generates a property declaration for an observable source member. + /// The source member metadata. + /// The ReactiveUI implementation namespace. + /// The generated property declaration. + private static string GetPropertySyntax(ObservableMethodInfo propertyInfo, string reactiveUiNamespace) { - var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedPropertyAttributes)); + var propertyAttributes = GetPropertyAttributes(propertyInfo); var getterFieldIdentifierName = propertyInfo.GetGeneratedFieldName(); var getterArrowExpression = propertyInfo.IsNullableType || propertyInfo.IsFromPartialProperty ? $"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)" @@ -308,16 +366,11 @@ private static string GetPropertySyntax(ObservableMethodInfo propertyInfo) var isPartialProperty = string.Empty; var propertyType = propertyInfo.ObservableType; - string? initVal; - if (propertyType.EndsWith("##string") || propertyType.EndsWith("##string?")) - { - initVal = $""" = "{propertyInfo.InitialValue}";"""; - } - else - { - initVal = $" = {propertyInfo.InitialValue};"; - } - + var isStringProperty = propertyType.EndsWith("##string", System.StringComparison.Ordinal) + || propertyType.EndsWith("##string?", System.StringComparison.Ordinal); + string? initVal = isStringProperty + ? $""" = "{propertyInfo.InitialValue}";""" + : $" = {propertyInfo.InitialValue};"; var initialValue = string.IsNullOrWhiteSpace(propertyInfo.InitialValue) ? ";" : initVal; if (propertyInfo.IsFromPartialProperty) { @@ -325,12 +378,12 @@ private static string GetPropertySyntax(ObservableMethodInfo propertyInfo) propertyType = propertyInfo.PartialPropertyType; } - var helperTypeName = $"{propertyInfo.AccessModifier} ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?"; + var helperTypeName = $"{propertyInfo.AccessModifier} {reactiveUiNamespace}.ObservableAsPropertyHelper<{propertyType}>?"; // If the property is readonly, we need to change the helper to be non-nullable if (propertyInfo.IsReadOnly == "readonly") { - helperTypeName = $"{propertyInfo.AccessModifier} readonly ReactiveUI.ObservableAsPropertyHelper<{propertyType}>"; + helperTypeName = $"{propertyInfo.AccessModifier} readonly {reactiveUiNamespace}.ObservableAsPropertyHelper<{propertyType}>"; } return $$""" @@ -347,6 +400,9 @@ private static string GetPropertySyntax(ObservableMethodInfo propertyInfo) """; } + /// Generates the initialization method for observable property helpers. + /// The observable property metadata. + /// The generated initialization method. private static string GetPropertyInitiliser(ObservableMethodInfo[] propertyInfos) { using var propertyInitilisers = ImmutableArrayBuilder.Rent(); @@ -379,4 +435,81 @@ protected void InitializeOAPH() } """; } + + /// Gets parent information for each generated property. + /// The generated property metadata. + /// The parent information for the generated properties. + private static TargetInfo?[] GetParentInfos(ObservableMethodInfo[] properties) + { + var parentInfos = new TargetInfo?[properties.Length]; + for (var index = 0; index < properties.Length; index++) + { + parentInfos[index] = properties[index].TargetInfo.ParentInfo; + } + + return parentInfos; + } + + /// Gets the generated property declarations. + /// The generated property metadata. + /// The ReactiveUI implementation namespace. + /// The generated property declarations. + private static string GetPropertyDeclarations(ObservableMethodInfo[] properties, string reactiveUiNamespace) + { + var builder = new StringBuilder(); + for (var index = 0; index < properties.Length; index++) + { + if (index > 0) + { + _ = builder.Append('\n'); + } + + _ = builder.Append(GetPropertySyntax(properties[index], reactiveUiNamespace)); + } + + return builder.ToString(); + } + + /// Gets attributes applied to a generated property. + /// The source member metadata. + /// The property attributes separated by generated-code indentation. + private static string GetPropertyAttributes(ObservableMethodInfo propertyInfo) + { + var builder = new StringBuilder(); + foreach (var attribute in AttributeDefinitions.ExcludeFromCodeCoverage) + { + AppendObservablePropertyAttribute(builder, attribute); + } + + foreach (var attribute in propertyInfo.ForwardedPropertyAttributes.AsImmutableArray()) + { + AppendObservablePropertyAttribute(builder, attribute); + } + + return builder.ToString(); + } + + /// Appends a generated property attribute with the required separator. + /// The destination builder. + /// The attribute source. + private static void AppendObservablePropertyAttribute(StringBuilder builder, string attribute) + { + if (builder.Length > 0) + { + _ = builder.Append("\n "); + } + + _ = builder.Append(attribute); + } + + /// Groups the settings declared by an ObservableAsProperty attribute. + /// The optional generated property name. + /// The optional generated property initial value. + /// The generated helper accessibility. + /// Whether a partial generated property is read-only. + private readonly record struct ObservablePropertySettings( + string? PropertyName, + string? InitialValue, + string UseProtectedModifier, + bool? IsReadOnly); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs index b123ae92..d154f0d5 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs @@ -1,23 +1,23 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. public sealed partial class ObservableAsPropertyGenerator { + /// Registers generation of observable-backed properties declared from methods or properties. + /// The incremental generator initialization context. private static void RunObservableAsPropertyFromObservable(in IncrementalGeneratorInitializationContext context) { // Gather info for all annotated command methods (starting from method declarations with at least one attribute) @@ -27,42 +27,50 @@ private static void RunObservableAsPropertyFromObservable(in IncrementalGenerato AttributeDefinitions.ObservableAsPropertyAttributeType, static (node, _) => node is MethodDeclarationSyntax or PropertyDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 }, static (context, token) => GetObservableInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) - .Collect(); + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .Collect() + .Combine(context.ReactiveUiIntegration()); // Generate the requested properties and methods context.RegisterSourceOutput(methodInfo, static (context, input) => { - foreach (var diagnostic in input.SelectMany(static x => x.Errors)) - { - // Output the diagnostics - context.ReportDiagnostic(diagnostic.ToDiagnostic()); - } - - // Gather all the properties that are valid and group them by the target information. - var groupedPropertyInfo = input - .Where(static x => x.Value != null) - .Select(static x => x.Value!).GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); - - if (groupedPropertyInfo.Length == 0) - { - return; - } + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + List> groupedPropertyInfo = []; - foreach (var grouping in groupedPropertyInfo) + foreach (var result in input.Left) { - var items = grouping.ToImmutableArray(); + foreach (var diagnostic in result.Errors.AsImmutableArray()) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } - if (items.Length == 0) + if (result.Value is not ObservableMethodInfo propertyInfo) { continue; } - var source = GenerateObservableSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + var targetInfo = propertyInfo.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedPropertyInfo.TryGetValue(key, out var properties)) + { + properties = []; + groupedPropertyInfo.Add(key, properties); + } + + properties.Add(propertyInfo); + } + + foreach (var grouping in groupedPropertyInfo) + { + var source = GenerateObservableSource( + grouping.Key.TargetName, + grouping.Key.TargetNamespace, + grouping.Key.TargetVisibility, + grouping.Key.TargetType, + grouping.Value.ToArray(), + input.Right); context.AddSource($"{grouping.Key.FileHintName}.ObservableAsPropertyFromObservable.g.cs", source); } }); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/Models/PropertyInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/Models/PropertyInfo.cs index 311c62c5..3ebdecc6 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/Models/PropertyInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/Models/PropertyInfo.cs @@ -1,15 +1,26 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model with gathered info on a given field. -/// +/// Contains the metadata needed to generate a reactive property. +/// The enclosing type metadata. +/// The property's fully-qualified type name. +/// The backing field name. +/// The generated property name. +/// Whether the property type is nullable-reference compatible. +/// Whether the setter should have a MemberNotNull attribute. +/// The attributes copied to the generated property. +/// The generated setter's access modifier. +/// The generated property's inheritance modifier. +/// The generated property's required modifier. +/// Whether the source member is a partial property. +/// The generated property's access modifier. +/// The additional property names to notify after assignment. +/// The XML documentation to copy to the generated property. internal sealed record PropertyInfo( TargetInfo TargetInfo, string TypeNameWithNullabilityAnnotations, diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs index cdceded1..140c0960 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs @@ -1,13 +1,14 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using System.Linq; +using System.Collections.Generic; using System.Threading; using Microsoft.CodeAnalysis; +#if ROSYLN_412 || ROSYLN_500 using Microsoft.CodeAnalysis.CSharp; +#endif using Microsoft.CodeAnalysis.CSharp.Syntax; using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; @@ -16,32 +17,52 @@ namespace ReactiveUI.SourceGenerators; -/// -/// ReactiveGenerator. -/// +/// Generates ReactiveUI-compatible properties from attributed fields and partial properties. /// public sealed partial class ReactiveGenerator { + /// Gets the fully-qualified name emitted in generated-code attributes. internal static readonly string GeneratorName = typeof(ReactiveGenerator).FullName!; + + /// Gets the generator assembly version emitted in generated-code attributes. internal static readonly string GeneratorVersion = typeof(ReactiveGenerator).Assembly.GetName().Version.ToString(); + /// The access-modifier value for an internal setter. + private const int InternalSetModifier = 2; + + /// The access-modifier value for a private setter. + private const int PrivateSetModifier = 3; + + /// The access-modifier value for a protected-internal setter. + private const int ProtectedInternalSetModifier = 4; + + /// The access-modifier value for a private-protected setter. + private const int PrivateProtectedSetModifier = 5; + + /// The access-modifier value for an init-only setter. + private const int InitSetModifier = 6; + + /// The inheritance value for an override property. + private const int OverrideInheritanceModifier = 2; + + /// The inheritance value for a new property. + private const int NewInheritanceModifier = 3; + #if ROSYLN_412 || ROSYLN_500 + /// Gets metadata for an attributed partial property. + /// The generator attribute context. + /// The cancellation token. + /// The property metadata, or when the property is unsupported. private static Result? GetPropertyInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { using var builder = ImmutableArrayBuilder.Rent(); var symbol = context.TargetSymbol; - if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType, out var attributeData)) { return default; } - if (symbol is not IPropertySymbol propertySymbol) - { - return default; - } - - if (!propertySymbol.IsPartialDefinition || propertySymbol.IsStatic) + if (symbol is not IPropertySymbol propertySymbol || !propertySymbol.IsPartialDefinition || propertySymbol.IsStatic) { return default; } @@ -56,141 +77,124 @@ public sealed partial class ReactiveGenerator return new(default, builder.ToImmutable()); } + return new(CreatePartialPropertyInfo(context, propertySymbol, attributeData, builder, token), builder.ToImmutable()); + } + + /// Creates metadata for a valid attributed partial property. + /// The generator attribute context. + /// The attributed property symbol. + /// The reactive attribute. + /// The diagnostic builder. + /// The cancellation token. + /// The generated property metadata. + private static PropertyInfo CreatePartialPropertyInfo( + in GeneratorAttributeSyntaxContext context, + IPropertySymbol propertySymbol, + AttributeData attributeData, + ImmutableArrayBuilder builder, + CancellationToken token) + { token.ThrowIfCancellationRequested(); + var propertyAccessModifier = GetAccessibilityText(propertySymbol.DeclaredAccessibility); + var setAccessModifier = GetSetAccessModifier(propertySymbol, propertyAccessModifier); + var inheritance = GetPropertyInheritance(propertySymbol); + var fieldName = GetPartialPropertyFieldName(context, propertySymbol); + propertySymbol.GetNullabilityInfo(context.SemanticModel, out var isReferenceTypeOrUnconstraindTypeParameter, out var includeMemberNotNullOnSetAccessor); + context.GetForwardedAttributes(builder, propertySymbol, ((PropertyDeclarationSyntax)context.TargetNode).AttributeLists, token, out var forwardedAttributesString); + token.ThrowIfCancellationRequested(); + return new( + TargetInfo.From(propertySymbol.ContainingType), + propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), + fieldName, + propertySymbol.Name, + isReferenceTypeOrUnconstraindTypeParameter, + includeMemberNotNullOnSetAccessor, + forwardedAttributesString, + setAccessModifier, + inheritance, + propertySymbol.IsRequired ? "required " : string.Empty, + true, + propertyAccessModifier, + GetAlsoNotifyValues(attributeData, propertySymbol.Name, context.SemanticModel, token), + GetXmlDocumentation(propertySymbol, token)); + } - // Get Property AccessModifier. - var propertyAccessModifier = propertySymbol.DeclaredAccessibility.ToString().ToLower(); - if (propertyAccessModifier?.Contains("and") == true) - { - propertyAccessModifier = propertyAccessModifier.Replace("and", " "); - } - else if (propertyAccessModifier?.Contains("or") == true) - { - propertyAccessModifier = propertyAccessModifier.Replace("or", " "); - } + /// Gets normalized C# accessibility text. + /// The Roslyn accessibility value. + /// The corresponding C# accessibility text. + private static string GetAccessibilityText(Accessibility accessibility) + { + var text = accessibility.ToString().ToLowerInvariant(); + return text.Contains("protectedandinternal", StringComparison.Ordinal) + ? "private protected" + : text.Replace("and", " ").Replace("or", " "); + } - token.ThrowIfCancellationRequested(); + /// Gets the generated setter modifier for a partial property. + /// The property symbol. + /// The generated property modifier. + /// The setter modifier text. + private static string GetSetAccessModifier(IPropertySymbol propertySymbol, string propertyAccessModifier) + { + var setAccessModifier = $"{GetAccessibilityText(propertySymbol.SetMethod?.DeclaredAccessibility ?? Accessibility.Public)} set"; + return setAccessModifier == "public set" || setAccessModifier == $"{propertyAccessModifier} set" + ? "set" + : setAccessModifier; + } - // Get Set AccessModifier. - var setAccessModifier = $"{propertySymbol.SetMethod?.DeclaredAccessibility} set".ToLower(); - if (setAccessModifier.StartsWith("public", StringComparison.Ordinal)) - { - setAccessModifier = "set"; - } - else if (setAccessModifier?.Contains("and") == true) - { - if (setAccessModifier.Contains("protectedandinternal")) - { - setAccessModifier = setAccessModifier.Replace("protectedandinternal", "private protected"); - } - else - { - setAccessModifier = setAccessModifier.Replace("and", " "); - } - } - else if (setAccessModifier?.Contains("or") == true) + /// Gets the inheritance modifier for a partial property. + /// The partial property symbol. + /// The inheritance modifier text. + private static string GetPropertyInheritance(IPropertySymbol propertySymbol) + { + if (propertySymbol.IsVirtual) { - setAccessModifier = setAccessModifier.Replace("or", " "); + return " virtual"; } - if (propertyAccessModifier == "private" && setAccessModifier == "private set") - { - setAccessModifier = "set"; - } - else if (propertyAccessModifier == "internal" && setAccessModifier == "internal set") - { - setAccessModifier = "set"; - } - else if (propertyAccessModifier == "protected" && setAccessModifier == "protected set") - { - setAccessModifier = "set"; - } - else if (propertyAccessModifier == "protected internal" && setAccessModifier == "protected internal set") - { - setAccessModifier = "set"; - } - else if (propertyAccessModifier == "private protected" && setAccessModifier == "private protected set") + return propertySymbol.IsOverride ? " override" : string.Empty; + } + + /// Gets the backing-field name used for an attributed partial property. + /// The generator attribute context. + /// The property symbol. + /// The field name supported by the target language version. + private static string GetPartialPropertyFieldName(in GeneratorAttributeSyntaxContext context, IPropertySymbol propertySymbol) => + context.SemanticModel.Compilation is CSharpCompilation { LanguageVersion: > LanguageVersion.CSharp13 } + ? "field" + : propertySymbol.GetGeneratedFieldName(); + + /// Formats symbol XML documentation for insertion into generated source. + /// The documented symbol. + /// The cancellation token. + /// The formatted documentation, or an empty string. + private static string GetXmlDocumentation(ISymbol symbol, CancellationToken token) + { + var xmlDocumentation = symbol.GetDocumentationCommentXml(cancellationToken: token) ?? string.Empty; + if (xmlDocumentation.Length == 0) { - setAccessModifier = "set"; + return string.Empty; } - token.ThrowIfCancellationRequested(); - - var inheritance = propertySymbol.IsVirtual ? " virtual" : propertySymbol.IsOverride ? " override" : string.Empty; - - var useRequired = propertySymbol.IsRequired ? "required " : string.Empty; - - var typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - var fieldName = propertySymbol.GetGeneratedFieldName(); - if (context.SemanticModel.Compilation is CSharpCompilation compilation && compilation.LanguageVersion > LanguageVersion.CSharp13) + var lines = xmlDocumentation.Split('\n'); + if (lines.Length < 3) { - fieldName = "field"; + return string.Empty; } - var propertyName = propertySymbol.Name; - - // Get the nullability info for the property - propertySymbol.GetNullabilityInfo( - context.SemanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); - - var propertyDeclaration = (PropertyDeclarationSyntax)context.TargetNode; - - context.GetForwardedAttributes( - builder, - propertySymbol, - propertyDeclaration.AttributeLists, - token, - out var forwardedAttributesString); - - token.ThrowIfCancellationRequested(); - - var alsoNotify = GetAlsoNotifyValues(attributeData, propertyName, context.SemanticModel, token); - - token.ThrowIfCancellationRequested(); - - // Get the containing type info - var targetInfo = TargetInfo.From(propertySymbol.ContainingType); - - token.ThrowIfCancellationRequested(); - - var xmlTrivia = propertySymbol.GetDocumentationCommentXml(cancellationToken: token); - - // Remove Member attributes from xmlTrivia on the first and last lines - if (xmlTrivia?.Length > 0) + var formattedDocumentation = new System.Text.StringBuilder(); + const int XmlMemberEnvelopeLineCount = 2; + for (var index = 1; index < lines.Length - XmlMemberEnvelopeLineCount; index++) { - var s = xmlTrivia.Split('\n').ToList(); - s.RemoveAt(0); - s.Remove(s.Last()); - s.Remove(s.Last()); - xmlTrivia = string.Concat(s.Select(c => " /// " + c.TrimStart() + "\n")); - xmlTrivia = xmlTrivia.TrimEnd(); + _ = formattedDocumentation.Append(" /// ") + .AppendLine(lines[index].TrimStart()); } - return new( - new( - targetInfo, - typeNameWithNullabilityAnnotations, - fieldName, - propertyName, - isReferenceTypeOrUnconstraindTypeParameter, - includeMemberNotNullOnSetAccessor, - forwardedAttributesString, - setAccessModifier!, - inheritance, - useRequired, - true, - propertyAccessModifier!, - alsoNotify, - xmlTrivia), - builder.ToImmutable()); + return formattedDocumentation.ToString().TrimEnd(); } #endif - /// - /// Gets the observable method information. - /// + /// Gets the observable method information. /// The context. /// The token. /// @@ -199,146 +203,129 @@ public sealed partial class ReactiveGenerator private static Result? GetVariableInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { using var builder = ImmutableArrayBuilder.Rent(); - var symbol = context.TargetSymbol; - - if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType, out var attributeData)) + if (!context.TargetSymbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType, out var attributeData)) { return default; } - if (symbol is not IFieldSymbol fieldSymbol) + if (context.TargetSymbol is not IFieldSymbol fieldSymbol || !fieldSymbol.IsTargetTypeValid()) { - return default; - } + if (context.TargetSymbol is not IFieldSymbol invalidFieldSymbol) + { + return default; + } - if (!fieldSymbol.IsTargetTypeValid()) - { builder.Add( InvalidReactiveObjectError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + invalidFieldSymbol, + invalidFieldSymbol.ContainingType, + invalidFieldSymbol.Name); return new(default, builder.ToImmutable()); } - token.ThrowIfCancellationRequested(); - - // Get AccessModifier enum value from the attribute - attributeData.TryGetNamedArgument("SetModifier", out int accessModifierArgument); - var setAccessModifier = accessModifierArgument switch - { - 1 => "protected set", - 2 => "internal set", - 3 => "private set", - 4 => "protected internal set", - 5 => "private protected set", - 6 => "init", - _ => "set", - }; - - token.ThrowIfCancellationRequested(); - - // Get Inheritance value from the attribute - attributeData.TryGetNamedArgument("Inheritance", out int inheritanceArgument); - var inheritance = inheritanceArgument switch - { - 1 => " virtual", - 2 => " override", - 3 => " new", - _ => string.Empty, - }; - - token.ThrowIfCancellationRequested(); - - // Get Inheritance value from the attribute - attributeData.TryGetNamedArgument("UseRequired", out bool useRequiredArgument); - var useRequired = useRequiredArgument ? "required " : string.Empty; - - token.ThrowIfCancellationRequested(); + var propertyInfo = CreateFieldPropertyInfo(context, fieldSymbol, attributeData, builder, token); + return propertyInfo is null ? new(default, builder.ToImmutable()) : new(propertyInfo, builder.ToImmutable()); + } - // Get the property type and name - var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - var fieldName = fieldSymbol.Name; + /// Creates metadata for a valid attributed field. + /// The generator attribute context. + /// The attributed field symbol. + /// The reactive attribute. + /// The diagnostic builder. + /// The cancellation token. + /// The generated property metadata, or for a name collision. + private static PropertyInfo? CreateFieldPropertyInfo( + in GeneratorAttributeSyntaxContext context, + IFieldSymbol fieldSymbol, + AttributeData attributeData, + ImmutableArrayBuilder builder, + CancellationToken token) + { var propertyName = fieldSymbol.GetGeneratedPropertyName(); - - if (fieldName == propertyName) + if (fieldSymbol.Name == propertyName) { - builder.Add( - ReactivePropertyNameCollisionError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); - return new(default, builder.ToImmutable()); + builder.Add(ReactivePropertyNameCollisionError, fieldSymbol, fieldSymbol.ContainingType, fieldSymbol.Name); + return null; } token.ThrowIfCancellationRequested(); - - // Get the nullability info for the property - fieldSymbol.GetNullabilityInfo( - context.SemanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); - - token.ThrowIfCancellationRequested(); - var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; - - context.GetForwardedAttributes( - builder, - fieldSymbol, - fieldDeclaration.AttributeLists, - token, - out var forwardedAttributesString); - - token.ThrowIfCancellationRequested(); - - var alsoNotify = GetAlsoNotifyValues(attributeData, propertyName, context.SemanticModel, token); - - token.ThrowIfCancellationRequested(); - - // Get the containing type info - var targetInfo = TargetInfo.From(fieldSymbol.ContainingType); - + fieldSymbol.GetNullabilityInfo(context.SemanticModel, out var isReferenceTypeOrUnconstraindTypeParameter, out var includeMemberNotNullOnSetAccessor); + context.GetForwardedAttributes(builder, fieldSymbol, ((FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!).AttributeLists, token, out var forwardedAttributesString); token.ThrowIfCancellationRequested(); - return new( - new( - targetInfo, - typeNameWithNullabilityAnnotations, - fieldName, + TargetInfo.From(fieldSymbol.ContainingType), + fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), + fieldSymbol.Name, propertyName, isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, forwardedAttributesString, - setAccessModifier, - inheritance, - useRequired, + GetFieldSetAccessModifier(attributeData), + GetFieldInheritance(attributeData), + attributeData.GetNamedArgument("UseRequired") ? "required " : string.Empty, false, "public", - alsoNotify, - string.Empty), - builder.ToImmutable()); + GetAlsoNotifyValues(attributeData, propertyName, context.SemanticModel, token), + string.Empty); } - /// - /// Generates the source code. - /// + /// Gets the generated setter modifier for an attributed field. + /// The reactive attribute. + /// The setter modifier text. + private static string GetFieldSetAccessModifier(AttributeData attributeData) => + attributeData.GetNamedArgument("SetModifier") switch + { + 1 => "protected set", + InternalSetModifier => "internal set", + PrivateSetModifier => "private set", + ProtectedInternalSetModifier => "protected internal set", + PrivateProtectedSetModifier => "private protected set", + InitSetModifier => "init", + _ => "set", + }; + + /// Gets the generated inheritance modifier for an attributed field. + /// The reactive attribute. + /// The inheritance modifier text. + private static string GetFieldInheritance(AttributeData attributeData) => + attributeData.GetNamedArgument("Inheritance") switch + { + 1 => " virtual", + OverrideInheritanceModifier => " override", + NewInheritanceModifier => " new", + _ => string.Empty, + }; + + /// Generates the source code. /// The contain type name. /// The containing namespace. /// The containing class visibility. /// The containing type. /// The properties. + /// The selected ReactiveUI API surface. /// The value. - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, PropertyInfo[] properties) + private static string GenerateSource( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + PropertyInfo[] properties, + ReactiveUiIntegration integration) { - // Get Parent class details from properties.ParentInfo - var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations([.. properties.Select(p => p.TargetInfo.ParentInfo)]); + var parentTypes = new TargetInfo?[properties.Length]; + for (var index = 0; index < properties.Length; index++) + { + parentTypes[index] = properties[index].TargetInfo.ParentInfo; + } - var classes = GenerateClassWithProperties(containingTypeName, containingNamespace, containingClassVisibility, containingType, properties); + var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(parentTypes); + + var classes = GenerateClassWithProperties(containingTypeName, containingClassVisibility, containingType, properties); return $$""" // -using ReactiveUI; +{{integration.UsingDirectives}} #pragma warning disable #nullable enable @@ -352,19 +339,26 @@ namespace {{containingNamespace}} """; } - /// - /// Generates the source code. - /// + /// Generates the source code. /// The contain type name. - /// The containing namespace. /// The containing class visibility. /// The containing type. /// The properties. /// The value. - private static string GenerateClassWithProperties(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, PropertyInfo[] properties) + private static string GenerateClassWithProperties(string containingTypeName, string containingClassVisibility, string containingType, PropertyInfo[] properties) { - // Includes 2 tabs from the property declarations so no need to add them here. - var propertyDeclarations = string.Join("\n", properties.Select(GetPropertySyntax)); + var propertyDeclarationsBuilder = new System.Text.StringBuilder(); + foreach (var property in properties) + { + if (propertyDeclarationsBuilder.Length > 0) + { + _ = propertyDeclarationsBuilder.AppendLine(); + } + + _ = propertyDeclarationsBuilder.Append(GetPropertySyntax(property)); + } + + var propertyDeclarations = propertyDeclarationsBuilder.ToString(); return $$""" @@ -375,9 +369,7 @@ private static string GenerateClassWithProperties(string containingTypeName, str """; } - /// - /// Generates property declarations for the given observable method information. - /// + /// Generates property declarations for the given observable method information. /// Metadata about the observable property. /// A string containing the generated code for the property. private static string GetPropertySyntax(PropertyInfo propertyInfo) @@ -387,155 +379,215 @@ private static string GetPropertySyntax(PropertyInfo propertyInfo) return string.Empty; } - var setFieldName = propertyInfo.FieldName; + var partialModifier = propertyInfo.IsProperty ? "partial " : string.Empty; var getFieldName = propertyInfo.FieldName; - if (propertyInfo.FieldName == "value") - { - setFieldName = "this.value"; + var setFieldName = getFieldName == "value" ? "this.value" : getFieldName; + var memberNotNullAttribute = GetMemberNotNullAttribute(propertyInfo, setFieldName); + var propertyDeclaration = GetPropertyDeclaration(propertyInfo, partialModifier); + var openingBrace = memberNotNullAttribute.Length > 0 + && propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?", StringComparison.Ordinal) + ? "{ " + : "{"; + return $$""" + {{GetFieldSyntax(propertyInfo)}} +{{GetDocumentationSyntax(propertyInfo, getFieldName)}} + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] + {{GetPropertyAttributes(propertyInfo)}} + {{propertyDeclaration}} + {{openingBrace}} + get => {{getFieldName}}; +{{memberNotNullAttribute}} {{propertyInfo.SetAccessModifier}} + { + this.RaiseAndSetIfChanged(ref {{setFieldName}}, value);{{GetAlsoNotifyStatements(propertyInfo.AlsoNotify)}} + } } +"""; + } - var fieldSyntax = string.Empty; - var partialModifier = propertyInfo.IsProperty ? "partial " : string.Empty; - string propertyAttributes; - - if (propertyInfo.IsProperty && propertyInfo.FieldName != "field") - { - propertyAttributes = string.Join("\n ", propertyInfo.ForwardedAttributes); - fieldSyntax = -$$""" -{{propertyAttributes}} + /// Gets the optional backing-field declaration for a generated property. + /// The generated property metadata. + /// The field declaration, or an empty string. + private static string GetFieldSyntax(PropertyInfo propertyInfo) => + !propertyInfo.IsProperty || propertyInfo.FieldName == "field" + ? string.Empty + : $$""" +{{JoinIndentedLines(propertyInfo.ForwardedAttributes)}} private {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.FieldName}}; """; - } - var docSource = string.Empty; - if (propertyInfo.IsProperty) - { - docSource = !string.IsNullOrWhiteSpace(propertyInfo.XmlComment) - ? propertyInfo.XmlComment - : $$""" /// """; + /// Gets the declaration line for a generated property. + /// The generated property metadata. + /// The partial modifier, when applicable. + /// The generated property declaration. + private static string GetPropertyDeclaration(PropertyInfo propertyInfo, string partialModifier) + { + var modifiers = $"{propertyInfo.PropertyAccessModifier}{propertyInfo.Inheritance} {propertyInfo.UseRequired}{partialModifier}"; + return $"{modifiers}{propertyInfo.TypeNameWithNullabilityAnnotations} {propertyInfo.PropertyName}"; + } - propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage); - } - else + /// Gets the documentation declaration for a generated property. + /// The generated property metadata. + /// The generated getter field name. + /// The documentation declaration. + private static string GetDocumentationSyntax(PropertyInfo propertyInfo, string getFieldName) + { + if (!propertyInfo.IsProperty) { - docSource = $$""" /// """; - propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); + return $$""" /// """; } - var alsoNotifyAttributes = string.Empty; - if (!propertyInfo.AlsoNotify.IsEmpty) + return string.IsNullOrWhiteSpace(propertyInfo.XmlComment) + ? $$""" /// """ + : propertyInfo.XmlComment!; + } + + /// Gets the attributes applied to a generated property. + /// The generated property metadata. + /// The formatted attribute list. + private static string GetPropertyAttributes(PropertyInfo propertyInfo) + { + var builder = new System.Text.StringBuilder(); + AppendIndentedLines(builder, AttributeDefinitions.ExcludeFromCodeCoverage); + if (!propertyInfo.IsProperty) { - alsoNotifyAttributes = string.Concat(propertyInfo.AlsoNotify.Select(an => $"\n this.RaisePropertyChanged(nameof({an}));")); + AppendIndentedLines(builder, propertyInfo.ForwardedAttributes); } - var accessModifier = propertyInfo.PropertyAccessModifier; - var setAccessModifier = propertyInfo.SetAccessModifier; + return builder.ToString(); + } - if (propertyInfo.IncludeMemberNotNullOnSetAccessor || propertyInfo.IsReferenceTypeOrUnconstrainedTypeParameter) + /// Gets the optional member-not-null accessor attribute. + /// The generated property metadata. + /// The field assigned by the setter. + /// The formatted attribute prefix, or an empty string. + private static string GetMemberNotNullAttribute(PropertyInfo propertyInfo, string setFieldName) => + propertyInfo.IncludeMemberNotNullOnSetAccessor || propertyInfo.IsReferenceTypeOrUnconstrainedTypeParameter + ? $" [global::System.Diagnostics.CodeAnalysis.MemberNotNull(\"{setFieldName}\")]\n" + : string.Empty; + + /// Gets property-changed statements for additional notifications. + /// The additional property names. + /// The generated statements. + private static string GetAlsoNotifyStatements(EquatableArray alsoNotify) + { + var builder = new System.Text.StringBuilder(); + foreach (var propertyName in alsoNotify.AsImmutableArray()) { - return -$$""" - {{fieldSyntax}} -{{docSource}} - [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] - {{propertyAttributes}} - {{accessModifier}}{{propertyInfo.Inheritance}} {{propertyInfo.UseRequired}}{{partialModifier}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} - { - get => {{getFieldName}}; - [global::System.Diagnostics.CodeAnalysis.MemberNotNull("{{setFieldName}}")] - {{setAccessModifier}} - { - this.RaiseAndSetIfChanged(ref {{setFieldName}}, value);{{alsoNotifyAttributes}} - } - } -"""; + _ = builder.Append("\n this.RaisePropertyChanged(nameof(") + .Append(propertyName) + .Append("));"); } - return -$$""" - {{fieldSyntax}} -{{docSource}} - [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] - {{propertyAttributes}} - {{accessModifier}}{{propertyInfo.Inheritance}} {{propertyInfo.UseRequired}}{{partialModifier}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} + return builder.ToString(); + } + + /// Joins source lines with generated-property indentation. + /// The source lines. + /// The joined and indented lines. + private static string JoinIndentedLines(IEnumerable lines) + { + var builder = new System.Text.StringBuilder(); + AppendIndentedLines(builder, lines); + return builder.ToString(); + } + + /// Appends generated-property source lines with indentation. + /// The destination builder. + /// The source lines. + private static void AppendIndentedLines(System.Text.StringBuilder builder, IEnumerable lines) + { + foreach (var line in lines) { - get => {{getFieldName}}; - {{setAccessModifier}} + if (builder.Length > 0) { - this.RaiseAndSetIfChanged(ref {{setFieldName}}, value);{{alsoNotifyAttributes}} + _ = builder.Append("\n "); } + + _ = builder.Append(line); } -"""; } + /// Gets the additional property names notified by a reactive property update. + /// The reactive attribute. + /// The generated property name. + /// The semantic model for syntax fallback. + /// The cancellation token. + /// The additional property names. private static EquatableArray GetAlsoNotifyValues(AttributeData attributeData, string propertyName, SemanticModel semanticModel, CancellationToken token) { using var builder = ImmutableArrayBuilder.Rent(); - - // Prefer the helper that flattens params arrays reliably foreach (var notify in attributeData.GetConstructorArguments()) { - if (string.IsNullOrWhiteSpace(notify) || string.Equals(notify, propertyName, StringComparison.Ordinal)) - { - continue; - } - - builder.Add(notify!); + AddAlsoNotifyValue(builder, notify, propertyName); } - // Fallback for safety with any remaining constructor arguments foreach (var argument in attributeData.ConstructorArguments) { - if (argument.Kind == TypedConstantKind.Array) - { - if (argument.Values.IsDefaultOrEmpty) - { - continue; - } - - foreach (var value in argument.Values) - { - if (value.Value is string notifyValue && - !string.IsNullOrWhiteSpace(notifyValue) && - !string.Equals(notifyValue, propertyName, StringComparison.Ordinal)) - { - builder.Add(notifyValue); - } - } - } - else if (argument.Value is string notifyValue && - !string.IsNullOrWhiteSpace(notifyValue) && - !string.Equals(notifyValue, propertyName, StringComparison.Ordinal)) - { - builder.Add(notifyValue); - } + AddAlsoNotifyArgument(builder, argument, propertyName); } - // If nothing was resolved, try reading the syntax and semantic model directly if (builder.Count == 0 && attributeData.ApplicationSyntaxReference?.GetSyntax(token) is AttributeSyntax attributeSyntax) { - var arguments = attributeSyntax.ArgumentList?.Arguments; - if (arguments is not null) - { - foreach (var argument in arguments.Value) - { - var constantValue = semanticModel.GetConstantValue(argument.Expression, token); - if (!constantValue.HasValue || constantValue.Value is not string notifyValue) - { - continue; - } - - if (string.IsNullOrWhiteSpace(notifyValue) || string.Equals(notifyValue, propertyName, StringComparison.Ordinal)) - { - continue; - } - - builder.Add(notifyValue); - } - } + AddAlsoNotifySyntaxArguments(builder, attributeSyntax, propertyName, semanticModel, token); } return builder.ToImmutable(); } + + /// Adds valid notification values represented by an attribute argument. + /// The destination builder. + /// The attribute argument. + /// The generated property name. + private static void AddAlsoNotifyArgument(ImmutableArrayBuilder builder, TypedConstant argument, string propertyName) + { + if (argument.Kind != TypedConstantKind.Array) + { + AddAlsoNotifyValue(builder, argument.Value as string, propertyName); + return; + } + + foreach (var value in argument.Values) + { + AddAlsoNotifyValue(builder, value.Value as string, propertyName); + } + } + + /// Adds valid notification values represented in attribute syntax. + /// The destination builder. + /// The attribute syntax. + /// The generated property name. + /// The semantic model for constant lookup. + /// The cancellation token. + private static void AddAlsoNotifySyntaxArguments( + ImmutableArrayBuilder builder, + AttributeSyntax attributeSyntax, + string propertyName, + SemanticModel semanticModel, + CancellationToken token) + { + if (attributeSyntax.ArgumentList is not { Arguments: var arguments }) + { + return; + } + + foreach (var argument in arguments) + { + var constantValue = semanticModel.GetConstantValue(argument.Expression, token); + AddAlsoNotifyValue(builder, constantValue.HasValue ? constantValue.Value as string : null, propertyName); + } + } + + /// Adds one valid additional-notification value. + /// The destination builder. + /// The candidate property name. + /// The generated property name. + private static void AddAlsoNotifyValue(ImmutableArrayBuilder builder, string? value, string propertyName) + { + if (string.IsNullOrWhiteSpace(value) || string.Equals(value, propertyName, StringComparison.Ordinal)) + { + return; + } + + builder.Add(value!); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs index c67b0909..b9915780 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs @@ -1,28 +1,29 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveGenerator : IIncrementalGenerator { + /// The number of accessors required for a partial property. + private const int RequiredAccessorCount = 2; + /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => { // Add the AccessModifier enum to the compilation ctx.AddSource($"{AttributeDefinitions.AccessModifierType}.g.cs", SourceText.From(AttributeDefinitions.GetAccessModifierEnum(), Encoding.UTF8)); @@ -37,102 +38,118 @@ public void Initialize(IncrementalGeneratorInitializationContext context) #endif } - private static void RunReactiveFromField(IncrementalGeneratorInitializationContext context) + /// Registers generation for fields annotated with ReactiveAttribute. + /// The incremental generator initialization context. + private static void RunReactiveFromField(in IncrementalGeneratorInitializationContext context) { // Gather info for all annotated variable with at least one attribute. var propertyInfo = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.ReactiveAttributeType, - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + static (node, _) => node is VariableDeclaratorSyntax + { + Parent.Parent: FieldDeclarationSyntax { AttributeLists.Count: > 0 }, + Parent.Parent.Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, + }, static (context, token) => GetVariableInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) - .Collect(); + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .Collect() + .Combine(context.ReactiveUiIntegration()); // Generate the requested properties context.RegisterSourceOutput(propertyInfo, static (context, input) => { - foreach (var diagnostic in input.SelectMany(static x => x.Errors)) - { - // Output the diagnostics - context.ReportDiagnostic(diagnostic.ToDiagnostic()); - } - - // Gather all the properties that are valid and group them by the target information. - var groupedPropertyInfo = input - .Where(static x => x.Value != null) - .Select(static x => x.Value!).GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + List> groupedPropertyInfo = []; - if (groupedPropertyInfo.Length == 0) - { - return; - } - - foreach (var grouping in groupedPropertyInfo) + foreach (var result in input.Left) { - var items = grouping.ToImmutableArray(); + foreach (var diagnostic in result.Errors.AsImmutableArray()) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } - if (items.Length == 0) + if (result.Value is not PropertyInfo propertyInfo) { continue; } - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + var targetInfo = propertyInfo.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedPropertyInfo.TryGetValue(key, out var properties)) + { + properties = []; + groupedPropertyInfo.Add(key, properties); + } + + properties.Add(propertyInfo); + } + + foreach (var grouping in groupedPropertyInfo) + { + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.Value.ToArray(), input.Right); context.AddSource($"{grouping.Key.FileHintName}.Properties.g.cs", source); } }); } #if ROSYLN_412 || ROSYLN_500 - private static void RunReactiveFromProperty(IncrementalGeneratorInitializationContext context) + /// Registers generation for partial properties annotated with ReactiveAttribute. + /// The incremental generator initialization context. + private static void RunReactiveFromProperty(in IncrementalGeneratorInitializationContext context) { // Gather info for all annotated variable with at least one attribute. var propertyInfo = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.ReactiveAttributeType, - static (node, _) => node is PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 }, + static (node, _) => node is PropertyDeclarationSyntax + { + AccessorList.Accessors.Count: RequiredAccessorCount, + AttributeLists.Count: > 0, + }, static (context, token) => GetPropertyInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) - .Collect(); + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .Collect() + .Combine(context.ReactiveUiIntegration()); // Generate the requested properties context.RegisterSourceOutput(propertyInfo, static (context, input) => { - foreach (var diagnostic in input.SelectMany(static x => x.Errors)) - { - // Output the diagnostics - context.ReportDiagnostic(diagnostic.ToDiagnostic()); - } + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + List> groupedPropertyInfo = []; - // Gather all the properties that are valid and group them by the target information. - var groupedPropertyInfo = input - .Where(static x => x.Value != null) - .Select(static x => x.Value!).GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); - - if (groupedPropertyInfo.Length == 0) + foreach (var result in input.Left) { - return; - } - - foreach (var grouping in groupedPropertyInfo) - { - var items = grouping.ToImmutableArray(); + foreach (var diagnostic in result.Errors.AsImmutableArray()) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } - if (items.Length == 0) + if (result.Value is not PropertyInfo propertyInfo) { continue; } - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + var targetInfo = propertyInfo.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedPropertyInfo.TryGetValue(key, out var properties)) + { + properties = []; + groupedPropertyInfo.Add(key, properties); + } + + properties.Add(propertyInfo); + } + + foreach (var grouping in groupedPropertyInfo) + { + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.Value.ToArray(), input.Right); context.AddSource($"{grouping.Key.FileHintName}.PartialProperties.g.cs", source); } }); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/Models/ReactiveCollectionFieldInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/Models/ReactiveCollectionFieldInfo.cs index 83d084f8..f047ba2c 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/Models/ReactiveCollectionFieldInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/Models/ReactiveCollectionFieldInfo.cs @@ -1,15 +1,20 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model with gathered info on a given field. -/// +/// A model with gathered info on a given field. +/// The target type that owns the generated property. +/// The property's fully qualified type name. +/// The source field name. +/// The generated property name. +/// The source field initializer, when present. +/// Whether the field type permits null. +/// Whether a member-not-null annotation is required. +/// Attributes copied to the generated property. internal sealed record ReactiveCollectionFieldInfo( TargetInfo TargetInfo, string TypeNameWithNullabilityAnnotations, diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.Execute.cs index 27bf7277..d186b401 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.Execute.cs @@ -1,14 +1,12 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Immutable; -using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; @@ -17,11 +15,13 @@ namespace ReactiveUI.SourceGenerators; -/// -/// ReactiveCollectionGenerator. -/// +/// Implements ReactiveCollection source generation. public sealed partial class ReactiveCollectionGenerator { + /// Creates metadata for a ReactiveCollection-annotated field. + /// The attribute syntax context for the field. + /// The cancellation token for the generator operation. + /// The field metadata and diagnostics, or when not applicable. private static Result? GetVariableInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { using var builder = ImmutableArrayBuilder.Rent(); @@ -29,7 +29,7 @@ public sealed partial class ReactiveCollectionGenerator token.ThrowIfCancellationRequested(); // Skip symbols without the target attribute - if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveCollectionAttributeType, out var attributeData)) + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveCollectionAttributeType, out _)) { return default; } @@ -50,10 +50,23 @@ public sealed partial class ReactiveCollectionGenerator return new(default, builder.ToImmutable()); } + return CreateResult(context, fieldSymbol, builder, token); + } + + /// Creates metadata for a validated ReactiveCollection field. + /// The attribute syntax context for the field. + /// The validated field symbol. + /// The diagnostics collected while processing the field. + /// The cancellation token for the generator operation. + /// The field metadata and diagnostics. + private static Result CreateResult( + in GeneratorAttributeSyntaxContext context, + IFieldSymbol fieldSymbol, + ImmutableArrayBuilder builder, + CancellationToken token) + { token.ThrowIfCancellationRequested(); - // Get the property type and name - var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); var fieldName = fieldSymbol.Name; var propertyName = fieldSymbol.GetGeneratedPropertyName(); @@ -68,8 +81,28 @@ public sealed partial class ReactiveCollectionGenerator return new(default, builder.ToImmutable()); } + return CreateMetadata(context, fieldSymbol, fieldName, propertyName, builder, token); + } + + /// Creates the generated property metadata for a non-conflicting field. + /// The attribute syntax context for the field. + /// The validated field symbol. + /// The source field name. + /// The generated property name. + /// The diagnostics collected while processing the field. + /// The cancellation token for the generator operation. + /// The field metadata and diagnostics. + private static Result CreateMetadata( + in GeneratorAttributeSyntaxContext context, + IFieldSymbol fieldSymbol, + string fieldName, + string propertyName, + ImmutableArrayBuilder builder, + CancellationToken token) + { + var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; - var initializer = fieldDeclaration.Declaration.Variables.FirstOrDefault()?.Initializer?.ToFullString(); + var initializer = GetInitializer(fieldDeclaration); token.ThrowIfCancellationRequested(); @@ -83,10 +116,8 @@ public sealed partial class ReactiveCollectionGenerator token.ThrowIfCancellationRequested(); // Get the nullability info for the property - fieldSymbol.GetNullabilityInfo( - context.SemanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); + var (isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor) = + GetNullabilityInfo(fieldSymbol, context.SemanticModel); token.ThrowIfCancellationRequested(); @@ -95,27 +126,65 @@ public sealed partial class ReactiveCollectionGenerator return new( new( - targetInfo, - typeNameWithNullabilityAnnotations, - fieldName, - propertyName, - initializer, - isReferenceTypeOrUnconstraindTypeParameter, - includeMemberNotNullOnSetAccessor, - forwardedPropertyAttributes), + targetInfo, + typeNameWithNullabilityAnnotations, + fieldName, + propertyName, + initializer, + isReferenceTypeOrUnconstraindTypeParameter, + includeMemberNotNullOnSetAccessor, + forwardedPropertyAttributes), builder.ToImmutable()); } - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ReactiveCollectionFieldInfo[] properties) + /// Gets nullability metadata for a generated reactive collection property. + /// The source field symbol. + /// The semantic model for the source field. + /// The reference-type and member-not-null metadata. + private static (bool IsReferenceType, bool IncludeMemberNotNull) GetNullabilityInfo( + IFieldSymbol fieldSymbol, + SemanticModel semanticModel) + { + fieldSymbol.GetNullabilityInfo( + semanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); + return (isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor); + } + + /// Gets the first declared field initializer. + /// The source field declaration. + /// The initializer text, when present. + private static string? GetInitializer(FieldDeclarationSyntax fieldDeclaration) + { + var variables = fieldDeclaration.Declaration.Variables; + return variables.Count > 0 ? variables[0].Initializer?.ToFullString() : null; + } + + /// Generates the complete source document for reactive collection declarations. + /// The name of the containing type. + /// The containing namespace. + /// The containing type visibility. + /// The containing type kind. + /// The generated property metadata. + /// The selected ReactiveUI integration metadata. + /// The generated source document. + private static string GenerateSource( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + ReactiveCollectionFieldInfo[] properties, + ReactiveUiIntegration integration) { // Get Parent class details from properties.ParentInfo - var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations([.. properties.Select(p => p.TargetInfo.ParentInfo)]); + var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(GetParentInfos(properties)); - var classes = GenerateClassWithProperties(containingTypeName, containingNamespace, containingClassVisibility, containingType, properties); + var classes = GenerateClassWithProperties(containingTypeName, containingClassVisibility, containingType, properties); return $$""" // -using ReactiveUI; +{{integration.UsingDirectives}} #pragma warning disable #nullable enable @@ -128,19 +197,21 @@ namespace {{containingNamespace}} """; } - /// - /// Generates the source code. - /// + /// Generates the source code. /// The contain type name. - /// The containing namespace. /// The containing class visibility. /// The containing type. /// The properties. /// The value. - private static string GenerateClassWithProperties(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ReactiveCollectionFieldInfo[] properties) + private static string GenerateClassWithProperties( + string containingTypeName, + string containingClassVisibility, + string containingType, + ReactiveCollectionFieldInfo[] properties) { // Includes 2 tabs from the property declarations so no need to add them here. - var propertyDeclarations = string.Join("\n", properties.Select(GetPropertySyntax)); + var propertyDeclarations = GetPropertyDeclarations(properties); + var collectionChangedDeclaration = GetCollectionChangedDeclaration(); return $$""" @@ -151,14 +222,27 @@ private static string GenerateClassWithProperties(string containingTypeName, str {{propertyDeclarations}} [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - private static global::System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged(IReactiveObject @this, string propName)=> (_, _) => @this.RaisePropertyChanged(propName); +{{collectionChangedDeclaration}} } """; } + /// Creates the collection-change helper while preserving its generated one-line format. + /// The generated collection-change helper declaration. + private static string GetCollectionChangedDeclaration() + { + var builder = new StringBuilder(" private static global::System.Collections.Specialized."); + _ = builder.Append("NotifyCollectionChangedEventHandler CollectionChanged(IReactiveObject @this, "); + _ = builder.Append("string propName)=> (_, _) => @this.RaisePropertyChanged(propName);"); + return builder.ToString(); + } + + /// Generates a property declaration for a reactive collection field. + /// The source field metadata. + /// The generated property declaration. private static string GetPropertySyntax(ReactiveCollectionFieldInfo propertyInfo) { - var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); + var propertyAttributes = GetPropertyAttributes(propertyInfo); return $$""" /// @@ -187,4 +271,69 @@ private static string GetPropertySyntax(ReactiveCollectionFieldInfo propertyInfo } """; } + + /// Gets parent information for each generated property. + /// The generated property metadata. + /// The parent information for the generated properties. + private static TargetInfo?[] GetParentInfos(ReactiveCollectionFieldInfo[] properties) + { + var parentInfos = new TargetInfo?[properties.Length]; + for (var index = 0; index < properties.Length; index++) + { + parentInfos[index] = properties[index].TargetInfo.ParentInfo; + } + + return parentInfos; + } + + /// Gets the generated property declarations. + /// The generated property metadata. + /// The generated property declarations. + private static string GetPropertyDeclarations(ReactiveCollectionFieldInfo[] properties) + { + var builder = new StringBuilder(); + for (var index = 0; index < properties.Length; index++) + { + if (index > 0) + { + _ = builder.Append('\n'); + } + + _ = builder.Append(GetPropertySyntax(properties[index])); + } + + return builder.ToString(); + } + + /// Gets attributes applied to a generated property. + /// The source field metadata. + /// The property attributes separated by generated-code indentation. + private static string GetPropertyAttributes(ReactiveCollectionFieldInfo propertyInfo) + { + var builder = new StringBuilder(); + foreach (var attribute in AttributeDefinitions.ExcludeFromCodeCoverage) + { + AppendPropertyAttribute(builder, attribute); + } + + foreach (var attribute in propertyInfo.ForwardedAttributes.AsImmutableArray()) + { + AppendPropertyAttribute(builder, attribute); + } + + return builder.ToString(); + } + + /// Appends a generated property attribute with the required separator. + /// The destination builder. + /// The attribute source. + private static void AppendPropertyAttribute(StringBuilder builder, string attribute) + { + if (builder.Length > 0) + { + _ = builder.Append("\n "); + } + + _ = builder.Append(attribute); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs index 4bf5e45f..7e190911 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs @@ -1,83 +1,95 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveCollectionGenerator : IIncrementalGenerator { + /// Gets the generator type name used in generated-code metadata. internal static readonly string GeneratorName = typeof(ReactiveCollectionGenerator).FullName!; + + /// Gets the generator assembly version used in generated-code metadata. internal static readonly string GeneratorVersion = typeof(ReactiveCollectionGenerator).Assembly.GetName().Version.ToString(); /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => { // Add the ReactiveAttribute to the compilation ctx.AddSource($"{AttributeDefinitions.ReactiveCollectionAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ReactiveCollectionAttribute, Encoding.UTF8)); }); - RunReactiveCollectionFromField(context); + RunReactiveCollectionFromField(in context); } - private static void RunReactiveCollectionFromField(IncrementalGeneratorInitializationContext context) + /// Registers the pipeline that generates properties from reactive collection fields. + /// The incremental generator initialization context. + private static void RunReactiveCollectionFromField(in IncrementalGeneratorInitializationContext context) { var propertyInfo = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.ReactiveCollectionAttributeType, - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + static (node, _) => node is VariableDeclaratorSyntax + { + Parent.Parent: FieldDeclarationSyntax + { + Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, + AttributeLists.Count: > 0, + }, + }, static (context, token) => GetVariableInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) - .Collect(); + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .Collect() + .Combine(context.ReactiveUiIntegration()); // Generate the requested properties and methods context.RegisterSourceOutput(propertyInfo, static (context, input) => { - foreach (var diagnostic in input.SelectMany(static x => x.Errors)) - { - // Output the diagnostics - context.ReportDiagnostic(diagnostic.ToDiagnostic()); - } - - // Gather all the properties that are valid and group them by the target information. - var groupedPropertyInfo = input - .Where(static x => x.Value != null) - .Select(static x => x.Value!).GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + List> groupedPropertyInfo = []; - if (groupedPropertyInfo.Length == 0) - { - return; - } - - foreach (var grouping in groupedPropertyInfo) + foreach (var result in input.Left) { - var items = grouping.ToImmutableArray(); + foreach (var diagnostic in result.Errors.AsImmutableArray()) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } - if (items.Length == 0) + if (result.Value is not ReactiveCollectionFieldInfo propertyInfo) { continue; } - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + var targetInfo = propertyInfo.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedPropertyInfo.TryGetValue(key, out var properties)) + { + properties = []; + groupedPropertyInfo.Add(key, properties); + } + + properties.Add(propertyInfo); + } + + foreach (var grouping in groupedPropertyInfo) + { + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.Value.ToArray(), input.Right); context.AddSource($"{grouping.Key.FileHintName}.ReactiveCollections.g.cs", source); } }); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CanExecuteTypeInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CanExecuteTypeInfo.cs index 10a5cf9a..9f4ee7ac 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CanExecuteTypeInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CanExecuteTypeInfo.cs @@ -1,13 +1,18 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerators.Models; +/// Describes the supported kinds of can-execute members. internal enum CanExecuteTypeInfo { + /// A property that provides an observable Boolean value. PropertyObservable, + + /// A method that returns an observable Boolean value. MethodObservable, + + /// A field that provides an observable Boolean value. FieldObservable, } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CommandInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CommandInfo.cs index f6bb1fe3..2c8dc2f2 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CommandInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/Models/CommandInfo.cs @@ -1,13 +1,26 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Models; -internal record CommandInfo( +/// Contains the metadata needed to generate a reactive command. +/// The enclosing type metadata. +/// The source method name. +/// The method return type. +/// The optional command argument type. +/// Whether the method returns a task. +/// Whether the command output is void. +/// Whether the method returns an observable. +/// The optional can-execute member name. +/// The kind of can-execute member. +/// The optional output scheduler expression. +/// Attributes copied to the generated property. +/// The generated property access modifier. +/// XML documentation copied to the generated property. +internal sealed record CommandInfo( TargetInfo TargetInfo, string MethodName, string MethodReturnType, @@ -22,13 +35,17 @@ internal record CommandInfo( string AccessModifier, string? XmlComment) { - private const string UnitTypeName = "global::System.Reactive.Unit"; + /// Gets the output type used by the generated command. + /// The framework-specific void type name. + /// The output type name. + internal string GetOutputTypeText(string voidTypeName) => IsReturnTypeVoid + ? voidTypeName + : MethodReturnType; - public string GetOutputTypeText() => IsReturnTypeVoid - ? UnitTypeName - : MethodReturnType; - - public string GetInputTypeText() => string.IsNullOrWhiteSpace(ArgumentType) - ? UnitTypeName - : ArgumentType!; + /// Gets the input type used by the generated command. + /// The framework-specific void type name. + /// The input type name. + internal string GetInputTypeText(string voidTypeName) => string.IsNullOrWhiteSpace(ArgumentType) + ? voidTypeName + : ArgumentType!; } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index 55a9cdf5..0b3be0d9 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -1,13 +1,12 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -17,126 +16,103 @@ namespace ReactiveUI.SourceGenerators; -/// -/// ReactiveCommandGenerator. -/// +/// Generates properties that expose methods as ReactiveUI commands. /// public partial class ReactiveCommandGenerator { + /// Gets the fully-qualified name emitted in generated-code attributes. internal static readonly string GeneratorName = typeof(ReactiveCommandGenerator).FullName!; + + /// Gets the generator assembly version emitted in generated-code attributes. internal static readonly string GeneratorVersion = typeof(ReactiveCommandGenerator).Assembly.GetName().Version.ToString(); - private const string ReactiveUI = "global::ReactiveUI"; + /// The ReactiveUI command type name. private const string ReactiveCommand = "ReactiveCommand"; - private const string RxCmd = ReactiveUI + "." + ReactiveCommand; + + /// The factory method used for synchronous commands. private const string Create = ".Create"; + + /// The factory method used for observable commands. private const string CreateO = ".CreateFromObservable"; + + /// The factory method used for task-backed commands. private const string CreateT = ".CreateFromTask"; + + /// The attribute property used to specify the can-execute member. private const string CanExecute = "CanExecute"; + + /// The attribute property used to specify the output scheduler. private const string OutputScheduler = "OutputScheduler"; - private const string MainThreadScheduler = ReactiveUI + ".RxSchedulers.MainThreadScheduler"; - private const string TaskpoolScheduler = ReactiveUI + ".RxSchedulers.TaskpoolScheduler"; - private static CommandInfo? GetMethodInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) + /// The access-modifier value for internal generated commands. + private const int InternalAccessibility = 2; + + /// The access-modifier value for private generated commands. + private const int PrivateAccessibility = 3; + + /// The access-modifier value for protected-internal generated commands. + private const int ProtectedInternalAccessibility = 4; + + /// The access-modifier value for private-protected generated commands. + private const int PrivateProtectedAccessibility = 5; + + /// The length of the conventional m_ field prefix. + private const int CommandNamePrefixLength = 2; + + /// Gets the metadata needed to generate a command for an attributed method. + /// The generator attribute context. + /// The cancellation token. + /// The command metadata, or when the method is unsupported. + private static CommandInfo? GetMethodInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) => + context.TargetSymbol is IMethodSymbol methodSymbol + ? CreateCommandInfo(context, methodSymbol, context.Attributes[0], token) + : default; + + /// Creates the metadata for a supported command method. + /// The generator attribute context. + /// The attributed method symbol. + /// The command attribute. + /// The cancellation token. + /// The command metadata, or when the method has unsupported parameters. + private static CommandInfo? CreateCommandInfo( + in GeneratorAttributeSyntaxContext context, + IMethodSymbol methodSymbol, + AttributeData attributeData, + CancellationToken token) { - var symbol = context.TargetSymbol; - - if (symbol is not IMethodSymbol methodSymbol) + var (isTask, isObservable, realReturnType, isReturnTypeVoid) = GetCommandReturnInfo(methodSymbol, context.SemanticModel.Compilation); + var methodParameters = GetCommandParameters(methodSymbol, isTask); + if (methodParameters.Length > 1) { return default; } - var attributeData = context.Attributes[0]; - - token.ThrowIfCancellationRequested(); - using var builder = ImmutableArrayBuilder.Rent(); - - var isTask = methodSymbol.ReturnType.IsTaskReturnType(); - var isObservable = methodSymbol.ReturnType.IsObservableReturnType(); - - var compilation = context.SemanticModel.Compilation; - var realReturnType = isTask || isObservable ? methodSymbol.ReturnType.GetTaskReturnType(compilation) : methodSymbol.ReturnType; - var isReturnTypeVoid = SymbolEqualityComparer.Default.Equals(realReturnType, compilation.GetSpecialType(SpecialType.System_Void)); - var hasCancellationToken = isTask && methodSymbol.Parameters.Any(x => x.Type.ToDisplayString() == "System.Threading.CancellationToken"); - - using var methodParameters = ImmutableArrayBuilder.Rent(); - if (hasCancellationToken && methodSymbol.Parameters.Length == 2) - { - methodParameters.Add(methodSymbol.Parameters[0]); - } - else if (!hasCancellationToken) - { - foreach (var parameter in methodSymbol.Parameters) - { - methodParameters.Add(parameter); - } - } - - if (methodParameters.Count > 1) - { - return default; // Too many parameters, continue - } - token.ThrowIfCancellationRequested(); - TryGetCanExecuteExpressionType(methodSymbol, attributeData, out var canExecuteObservableName, out var canExecuteTypeInfo); - token.ThrowIfCancellationRequested(); - - TryGetOutputScheduler(methodSymbol, attributeData, out var outputScheduler); - + TryGetOutputScheduler(methodSymbol, attributeData, context.SemanticModel.Compilation.GetReactiveUiIntegration(), out var outputScheduler); token.ThrowIfCancellationRequested(); - - // Get AccessModifier enum value from the attribute - var accessModifier = attributeData.GetNamedArgument("AccessModifier") switch - { - 1 => "protected", - 2 => "internal", - 3 => "private", - 4 => "protected internal", - 5 => "private protected", - _ => "public", - }; - + var accessModifier = GetAccessModifier(attributeData); token.ThrowIfCancellationRequested(); - + using var builder = ImmutableArrayBuilder.Rent(); var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; - context.GetForwardedAttributes( builder, methodSymbol, methodSyntax.AttributeLists, token, out var forwardedPropertyAttributes); - token.ThrowIfCancellationRequested(); - - // Get the containing type info var targetInfo = TargetInfo.From(methodSymbol.ContainingType); - token.ThrowIfCancellationRequested(); - - var argumentType = methodParameters.ToImmutable().SingleOrDefault()?.Type; - var argumentTypeString = argumentType?.GetFullyQualifiedNameWithNullabilityAnnotations(); - + var argumentTypeString = methodParameters.IsEmpty + ? null + : methodParameters[0].Type.GetFullyQualifiedNameWithNullabilityAnnotations(); token.ThrowIfCancellationRequested(); - var xmlTrivia = methodSymbol.GetDocumentationCommentXml(cancellationToken: token); - - // Remove Member attributes from xmlTrivia on the first and last lines - if (xmlTrivia?.Length > 0) - { - var s = xmlTrivia.Split('\n').ToList(); - s.RemoveAt(0); - s.Remove(s.Last()); - s.Remove(s.Last()); - xmlTrivia = string.Concat(s.Select(c => " /// " + c.TrimStart() + "\n")); - xmlTrivia = xmlTrivia.TrimEnd(); - } - return new( targetInfo, - symbol.Name, + methodSymbol.Name, realReturnType.GetFullyQualifiedNameWithNullabilityAnnotations(), argumentTypeString, isTask, @@ -147,15 +123,125 @@ public partial class ReactiveCommandGenerator outputScheduler, forwardedPropertyAttributes, accessModifier, - xmlTrivia); + GetXmlDocumentation(methodSymbol, token)); } - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, CommandInfo[] commands) + /// Gets the relevant return-type details for a command method. + /// The command method. + /// The active compilation. + /// The async shape, unwrapped return type, and void status. + private static (bool IsTask, bool IsObservable, ITypeSymbol ReturnType, bool IsVoid) GetCommandReturnInfo(IMethodSymbol methodSymbol, Compilation compilation) + { + var isTask = methodSymbol.ReturnType.IsTaskReturnType(); + var isObservable = methodSymbol.ReturnType.IsObservableReturnType(); + var returnType = isTask || isObservable + ? methodSymbol.ReturnType.GetTaskReturnType(compilation) + : methodSymbol.ReturnType; + var isVoid = SymbolEqualityComparer.Default.Equals(returnType, compilation.GetSpecialType(SpecialType.System_Void)); + return (isTask, isObservable, returnType, isVoid); + } + + /// Gets the supported input parameters for a command method. + /// The command method. + /// Whether the method is task-backed. + /// The parameters exposed by the generated command. + private static ImmutableArray GetCommandParameters(IMethodSymbol methodSymbol, bool isTask) + { + using var builder = ImmutableArrayBuilder.Rent(); + var hasCancellationToken = false; + foreach (var parameter in methodSymbol.Parameters) + { + if (parameter.Type.ToDisplayString() == "System.Threading.CancellationToken") + { + hasCancellationToken = true; + break; + } + } + + if (isTask && hasCancellationToken && methodSymbol.Parameters.Length == 2) + { + builder.Add(methodSymbol.Parameters[0]); + } + else if (!hasCancellationToken) + { + foreach (var parameter in methodSymbol.Parameters) + { + builder.Add(parameter); + } + } + + return builder.ToImmutable(); + } + + /// Gets the generated command property's access modifier. + /// The command attribute. + /// The C# access-modifier text. + private static string GetAccessModifier(AttributeData attributeData) => + attributeData.GetNamedArgument("AccessModifier") switch + { + 1 => "protected", + InternalAccessibility => "internal", + PrivateAccessibility => "private", + ProtectedInternalAccessibility => "protected internal", + PrivateProtectedAccessibility => "private protected", + _ => "public", + }; + + /// Formats a method's XML documentation for insertion into generated source. + /// The documented method. + /// The cancellation token. + /// The formatted documentation, or an empty string. + private static string GetXmlDocumentation(IMethodSymbol methodSymbol, CancellationToken token) + { + var xmlDocumentation = methodSymbol.GetDocumentationCommentXml(cancellationToken: token) ?? string.Empty; + if (string.IsNullOrEmpty(xmlDocumentation)) + { + return string.Empty; + } + + var lines = xmlDocumentation.Split('\n'); + if (lines.Length < 3) + { + return string.Empty; + } + + var formattedDocumentation = new System.Text.StringBuilder(); + const int XmlMemberEnvelopeLineCount = 2; + for (var index = 1; index < lines.Length - XmlMemberEnvelopeLineCount; index++) + { + _ = formattedDocumentation.Append(" /// ") + .AppendLine(lines[index].TrimStart()); + } + + return formattedDocumentation.ToString().TrimEnd(); + } + + /// Generates the complete source file for a target type's commands. + /// The containing type name. + /// The containing namespace. + /// The containing type visibility. + /// The containing type keyword. + /// The command metadata. + /// The selected ReactiveUI API surface. + /// The generated source file. + private static string GenerateSource( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + CommandInfo[] commands, + ReactiveUiIntegration integration) { // Get Parent class details from properties.ParentInfo - var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations([.. commands.Select(p => p.TargetInfo.ParentInfo)]); + var parentTypes = new TargetInfo?[commands.Length]; + for (var index = 0; index < commands.Length; index++) + { + parentTypes[index] = commands[index].TargetInfo.ParentInfo; + } + + var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(parentTypes); - var classes = GenerateClassWithCommands(containingTypeName, containingNamespace, containingClassVisibility, containingType, commands); + var classes = GenerateClassWithCommands(containingTypeName, containingClassVisibility, containingType, commands, integration); return $$""" @@ -173,19 +259,33 @@ namespace {{containingNamespace}} """; } - /// - /// Generates the source code. - /// + /// Generates the source code. /// The contain type name. - /// The containing namespace. /// The containing class visibility. /// The containing type. /// The commands. + /// The selected ReactiveUI API surface. /// The value. - private static string GenerateClassWithCommands(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, CommandInfo[] commands) + private static string GenerateClassWithCommands( + string containingTypeName, + string containingClassVisibility, + string containingType, + CommandInfo[] commands, + ReactiveUiIntegration integration) { // Includes 2 tabs from the property declarations so no need to add them here. - var commandDeclarations = string.Join("\n", commands.Select(GetCommandSyntax)); + var commandDeclarationsBuilder = new System.Text.StringBuilder(); + foreach (var command in commands) + { + if (commandDeclarationsBuilder.Length > 0) + { + _ = commandDeclarationsBuilder.AppendLine(); + } + + _ = commandDeclarationsBuilder.Append(GetCommandSyntax(command, integration)); + } + + var commandDeclarations = commandDeclarationsBuilder.ToString(); return $$""" @@ -197,84 +297,119 @@ private static string GenerateClassWithCommands(string containingTypeName, strin """; } - private static string GetCommandSyntax(CommandInfo commandExtensionInfo) + /// Generates the property declaration for a command. + /// The command metadata. + /// The selected ReactiveUI API surface. + /// The generated property declaration. + private static string GetCommandSyntax(CommandInfo commandExtensionInfo, ReactiveUiIntegration integration) { - var outputType = commandExtensionInfo.GetOutputTypeText(); - var inputType = commandExtensionInfo.GetInputTypeText(); + var outputType = commandExtensionInfo.GetOutputTypeText(integration.VoidTypeName); + var inputType = commandExtensionInfo.GetInputTypeText(integration.VoidTypeName); + var rxCmd = $"{integration.Namespace}.{ReactiveCommand}"; var commandName = GetGeneratedCommandName(commandExtensionInfo.MethodName, commandExtensionInfo.IsTask); var fieldName = GetGeneratedFieldName(commandName); - string? initializer; - if (commandExtensionInfo.ArgumentType == null) - { - initializer = GenerateBasicCommand(commandExtensionInfo, fieldName); - } - else if (commandExtensionInfo.ArgumentType != null && commandExtensionInfo.IsReturnTypeVoid) - { - initializer = GenerateInCommand(commandExtensionInfo, fieldName, inputType); - } - else if (commandExtensionInfo.ArgumentType != null && !commandExtensionInfo.IsReturnTypeVoid) - { - initializer = GenerateInOutCommand(commandExtensionInfo, fieldName, outputType, inputType); - } - else - { - return string.Empty; - } + var initializer = GetCommandInitializer(commandExtensionInfo, fieldName, outputType, inputType, rxCmd); // Prepare any forwarded property attributes - var forwardedPropertyAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(commandExtensionInfo.ForwardedPropertyAttributes)); + var forwardedPropertyAttributesString = GetForwardedPropertyAttributes(commandExtensionInfo); return $$""" - private {{RxCmd}}<{{inputType}}, {{outputType}}>? {{fieldName}}; + private {{rxCmd}}<{{inputType}}, {{outputType}}>? {{fieldName}}; {{commandExtensionInfo.XmlComment}} [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] {{forwardedPropertyAttributesString}} - {{commandExtensionInfo.AccessModifier}} {{RxCmd}}<{{inputType}}, {{outputType}}> {{commandName}} { get => {{initializer}} } + {{commandExtensionInfo.AccessModifier}} {{rxCmd}}<{{inputType}}, {{outputType}}> {{commandName}} { get => {{initializer}} } """; + } - static string GenerateBasicCommand(CommandInfo commandExtensionInfo, string fieldName) + /// Creates the initializer expression for a generated command. + /// The command metadata. + /// The backing-field name. + /// The generated command output type. + /// The generated command input type. + /// The fully-qualified reactive command type name. + /// The lazy-initializer expression. + private static string GetCommandInitializer(CommandInfo commandInfo, string fieldName, string outputType, string inputType, string commandTypeName) + { + var genericTypeArguments = string.Empty; + if (commandInfo.ArgumentType is not null) { - var commandType = commandExtensionInfo.IsObservable ? CreateO : commandExtensionInfo.IsTask ? CreateT : Create; - var outputScheduler = string.IsNullOrEmpty(commandExtensionInfo.OutputScheduler) ? string.Empty : $", outputScheduler: {commandExtensionInfo.OutputScheduler}"; - if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) - { - return $"{fieldName} ??= {RxCmd}{commandType}({commandExtensionInfo.MethodName}{outputScheduler});"; - } + genericTypeArguments = commandInfo.IsReturnTypeVoid + ? $"<{inputType}>" + : $"<{inputType}, {outputType}>"; + } + + var commandType = GetCommandFactoryMethod(commandInfo); + var canExecuteArgument = GetCanExecuteArgument(commandInfo); + var schedulerArgument = string.IsNullOrEmpty(commandInfo.OutputScheduler) + ? string.Empty + : $", outputScheduler: {commandInfo.OutputScheduler}"; + return $"{fieldName} ??= {commandTypeName}{commandType}{genericTypeArguments}({commandInfo.MethodName}{canExecuteArgument}{schedulerArgument});"; + } - return $"{fieldName} ??= {RxCmd}{commandType}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)}{outputScheduler});"; + /// Gets the ReactiveCommand factory method for the command shape. + /// The command metadata. + /// The factory method name. + private static string GetCommandFactoryMethod(CommandInfo commandInfo) + { + if (commandInfo.IsObservable) + { + return CreateO; } - static string GenerateInOutCommand(CommandInfo commandExtensionInfo, string fieldName, string outputType, string inputType) + return commandInfo.IsTask ? CreateT : Create; + } + + /// Gets the optional can-execute argument for a command factory call. + /// The command metadata. + /// The formatted can-execute argument, or an empty string. + private static string GetCanExecuteArgument(CommandInfo commandInfo) + { + if (string.IsNullOrEmpty(commandInfo.CanExecuteObservableName)) { - var commandType = commandExtensionInfo.IsObservable ? CreateO : commandExtensionInfo.IsTask ? CreateT : Create; - var outputScheduler = string.IsNullOrEmpty(commandExtensionInfo.OutputScheduler) ? string.Empty : $", outputScheduler: {commandExtensionInfo.OutputScheduler}"; - if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) - { - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}{outputScheduler});"; - } + return string.Empty; + } + + var invocationSuffix = commandInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty; + return $", {commandInfo.CanExecuteObservableName}{invocationSuffix}"; + } - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)}{outputScheduler});"; + /// Formats the attributes forwarded to a generated command property. + /// The command metadata. + /// The formatted attribute list. + private static string GetForwardedPropertyAttributes(CommandInfo commandInfo) + { + var builder = new System.Text.StringBuilder(); + foreach (var attribute in AttributeDefinitions.ExcludeFromCodeCoverage) + { + AppendForwardedAttribute(builder, attribute); } - static string GenerateInCommand(CommandInfo commandExtensionInfo, string fieldName, string inputType) + foreach (var attribute in commandInfo.ForwardedPropertyAttributes.AsImmutableArray()) { - var commandType = commandExtensionInfo.IsTask ? CreateT : Create; - var outputScheduler = string.IsNullOrEmpty(commandExtensionInfo.OutputScheduler) ? string.Empty : $", outputScheduler: {commandExtensionInfo.OutputScheduler}"; - if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) - { - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName}{outputScheduler});"; - } + AppendForwardedAttribute(builder, attribute); + } - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)}{outputScheduler});"; + return builder.ToString(); + } + + /// Appends an indented forwarded attribute to a source builder. + /// The target builder. + /// The attribute text. + private static void AppendForwardedAttribute(System.Text.StringBuilder builder, string attribute) + { + if (builder.Length > 0) + { + _ = builder.Append("\n "); } + + _ = builder.Append(attribute); } - /// - /// Tries to get the expression type for the "CanExecute" property, if available. - /// + /// Tries to get the expression type for the "CanExecute" property, if available. /// The input instance to process. /// The instance for . /// The resulting can execute member name, if available. @@ -296,12 +431,25 @@ private static void TryGetCanExecuteExpressionType( if (memberName is null) { - goto Failure; + canExecuteMemberName = null; + canExecuteTypeInfo = null; + return; } - var canExecuteSymbols = methodSymbol.ContainingType!.GetAllMembers(memberName).ToImmutableArray(); + var canExecuteSymbols = methodSymbol.ContainingType!.GetAllMembers(memberName); + var symbolCount = 0; + ISymbol? canExecuteSymbol = null; + foreach (var symbol in canExecuteSymbols) + { + symbolCount++; + canExecuteSymbol = symbol; + if (symbolCount > 1) + { + break; + } + } - if (canExecuteSymbols.IsEmpty) + if (symbolCount == 0) { // Special case for when the target member is a generated property from [ObservableProperty] if (TryGetCanExecuteMemberFromGeneratedProperty(memberName, methodSymbol.ContainingType, out canExecuteTypeInfo)) @@ -311,114 +459,118 @@ private static void TryGetCanExecuteExpressionType( return; } } - else if (canExecuteSymbols.Length > 1) - { - goto Failure; - } - else if (TryGetCanExecuteExpressionFromSymbol(canExecuteSymbols[0], out canExecuteTypeInfo)) + else if (symbolCount == 1 + && TryGetCanExecuteExpressionFromSymbol(canExecuteSymbol!, out canExecuteTypeInfo)) { canExecuteMemberName = memberName; return; } - Failure: canExecuteMemberName = null; canExecuteTypeInfo = null; - - return; } + /// Gets the configured output scheduler, when its member is valid. + /// The attributed command method. + /// The command attribute. + /// The selected ReactiveUI API surface. + /// The scheduler expression, when valid. private static void TryGetOutputScheduler( IMethodSymbol methodSymbol, AttributeData attributeData, + ReactiveUiIntegration integration, out string? outputScheduler) { - if (!attributeData.TryGetNamedArgument(OutputScheduler, out string? scheduler)) - { - outputScheduler = null; - return; - } - - if (scheduler is null) + if (!attributeData.TryGetNamedArgument(OutputScheduler, out string? scheduler) || scheduler is null) { outputScheduler = null; return; } - if (scheduler == MainThreadScheduler || scheduler == TaskpoolScheduler) + if (IsReactiveUiScheduler(scheduler)) { outputScheduler = scheduler; return; } - var outputSchedulerSymbols = methodSymbol.ContainingType!.GetAllMembers(scheduler).ToImmutableArray(); - if (outputSchedulerSymbols.IsEmpty) + if (!TryGetSingleMember(methodSymbol.ContainingType!.GetAllMembers(scheduler), out var outputSchedulerSymbol)) { outputScheduler = null; return; } - if (outputSchedulerSymbols.Length > 1) - { - outputScheduler = null; - return; - } + _ = TryGetOutputSchedulerFromSymbol(outputSchedulerSymbol, integration.Api, out outputScheduler); + } - if (TryGetOutputSchedulerFromSymbol(outputSchedulerSymbols[0], out outputScheduler)) + /// Determines whether a scheduler expression names a built-in ReactiveUI scheduler. + /// The scheduler expression. + /// when the expression is a built-in scheduler. + private static bool IsReactiveUiScheduler(string scheduler) => + scheduler is "global::ReactiveUI.RxSchedulers.MainThreadScheduler" + or "global::ReactiveUI.RxSchedulers.TaskpoolScheduler" + or "global::ReactiveUI.Reactive.RxSchedulers.MainThreadScheduler" + or "global::ReactiveUI.Reactive.RxSchedulers.TaskpoolScheduler"; + + /// Gets the only symbol from a candidate sequence. + /// The candidate symbols. + /// The only symbol, when present. + /// when exactly one symbol exists. + private static bool TryGetSingleMember(IEnumerable symbols, [NotNullWhen(true)] out ISymbol? symbol) + { + symbol = null; + foreach (var candidate in symbols) { - return; + if (symbol is not null) + { + return false; + } + + symbol = candidate; } - outputScheduler = null; + return symbol is not null; } + /// Validates a candidate scheduler symbol and gets its expression. + /// The candidate scheduler symbol. + /// The selected ReactiveUI API. + /// The scheduler expression, when valid. + /// when the symbol is a supported scheduler. private static bool TryGetOutputSchedulerFromSymbol( ISymbol outputSchedulerSymbol, + ReactiveUiApi api, [NotNullWhen(true)] out string? outputScheduler) { - if (outputSchedulerSymbol is IFieldSymbol outputSchedulerFieldSymbol) + switch (outputSchedulerSymbol) { - // The property type must always be a bool - if (!outputSchedulerFieldSymbol.Type.IsISchedulerType()) + case IFieldSymbol fieldSymbol when fieldSymbol.Type.IsSchedulerType(api): { - goto Failure; + outputScheduler = fieldSymbol.Name; + return true; } - outputScheduler = outputSchedulerFieldSymbol.Name; - return true; - } - else if (outputSchedulerSymbol is IPropertySymbol { GetMethod: not null } outputSchedulerPropertySymbol) - { - // The property type must always be a bool - if (!outputSchedulerPropertySymbol.Type.IsISchedulerType()) + case IPropertySymbol { GetMethod: not null } propertySymbol when propertySymbol.Type.IsSchedulerType(api): { - goto Failure; + outputScheduler = propertySymbol.Name; + return true; } - outputScheduler = outputSchedulerPropertySymbol.Name; - return true; - } - else if (outputSchedulerSymbol is IMethodSymbol outputSchedulerMethodSymbol) - { - // The return type must always be a bool - if (!outputSchedulerMethodSymbol.ReturnType.IsISchedulerType()) + case IMethodSymbol methodSymbol when methodSymbol.ReturnType.IsSchedulerType(api): { - goto Failure; + outputScheduler = methodSymbol.Name; + return true; } - outputScheduler = outputSchedulerMethodSymbol.Name; - return true; + default: + { + outputScheduler = null; + return false; + } } - - Failure: - outputScheduler = null; - return false; } - /// - /// Gets the expression type for the can execute logic, if possible. - /// + /// Gets the expression type for the can execute logic, if possible. /// The can execute member symbol (either a method or a property). /// The resulting can execute expression type, if available. /// Whether or not was set and the input symbol was valid. @@ -426,62 +578,35 @@ private static bool TryGetCanExecuteExpressionFromSymbol( ISymbol canExecuteSymbol, [NotNullWhen(true)] out CanExecuteTypeInfo? canExecuteTypeInfo) { - if (canExecuteSymbol is IMethodSymbol canExecuteMethodSymbol) + switch (canExecuteSymbol) { - // The return type must always be a bool - if (!canExecuteMethodSymbol.ReturnType.IsObservableBoolType()) + case IMethodSymbol methodSymbol when methodSymbol.ReturnType.IsObservableBoolType() && methodSymbol.Parameters.IsEmpty: { - goto Failure; + canExecuteTypeInfo = CanExecuteTypeInfo.MethodObservable; + return true; } - // If the method has parameters, it has to have a single one matching the command type - if (canExecuteMethodSymbol.Parameters.Length == 1) + case IPropertySymbol { GetMethod: not null } propertySymbol when propertySymbol.Type.IsObservableBoolType(): { - goto Failure; + canExecuteTypeInfo = CanExecuteTypeInfo.PropertyObservable; + return true; } - // Parameterless methods are always valid - if (canExecuteMethodSymbol.Parameters.IsEmpty) + case IFieldSymbol fieldSymbol when fieldSymbol.Type.IsObservableBoolType(): { - canExecuteTypeInfo = CanExecuteTypeInfo.MethodObservable; - + canExecuteTypeInfo = CanExecuteTypeInfo.FieldObservable; return true; } - } - else if (canExecuteSymbol is IPropertySymbol { GetMethod: not null } canExecutePropertySymbol) - { - // The property type must always be a bool - if (!canExecutePropertySymbol.Type.IsObservableBoolType()) - { - goto Failure; - } - - canExecuteTypeInfo = CanExecuteTypeInfo.PropertyObservable; - return true; - } - else if (canExecuteSymbol is IFieldSymbol canExecuteFieldSymbol) - { - // The property type must always be a bool - if (!canExecuteFieldSymbol.Type.IsObservableBoolType()) + default: { - goto Failure; + canExecuteTypeInfo = null; + return false; } - - canExecuteTypeInfo = CanExecuteTypeInfo.FieldObservable; - - return true; } - - Failure: - canExecuteTypeInfo = null; - - return false; } - /// - /// Gets the expression type for the can execute logic, if possible. - /// + /// Gets the expression type for the can execute logic, if possible. /// The member name passed to [ReactiveCommand(CanExecute = ...)]. /// The containing type for the method annotated with [ReactiveCommand]. /// The resulting can execute expression type, if available. @@ -499,12 +624,8 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty( continue; } - var attributes = memberSymbol.GetAttributes(); - // Only filter fields with the [Reactive] attribute - if (memberSymbol is IFieldSymbol && - !attributes.Any(static a => a.AttributeClass?.HasFullyQualifiedMetadataName( - AttributeDefinitions.ReactiveAttributeType) == true)) + if (!HasReactiveAttribute(memberSymbol)) { continue; } @@ -526,27 +647,50 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty( return false; } + /// Determines whether a symbol has the ReactiveAttribute. + /// The symbol to inspect. + /// when the attribute is present. + private static bool HasReactiveAttribute(ISymbol symbol) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType) == true) + { + return true; + } + } + + return false; + } + + /// Gets the generated command property name for a method. + /// The source method name. + /// Whether the method is asynchronous. + /// The generated command property name. private static string GetGeneratedCommandName(string methodName, bool isAsync) { var commandName = methodName; - if (commandName.StartsWith("m_")) + if (commandName.StartsWith("m_", System.StringComparison.Ordinal)) { - commandName = commandName.Substring(2); + commandName = commandName[CommandNamePrefixLength..]; } - else if (commandName.StartsWith("_")) + else if (commandName.StartsWith("_", System.StringComparison.Ordinal)) { commandName = commandName.TrimStart('_'); } - if (commandName.EndsWith("Async") && isAsync) + if (commandName.EndsWith("Async", System.StringComparison.Ordinal) && isAsync) { commandName = commandName.Substring(0, commandName.Length - "Async".Length); } - return $"{char.ToUpper(commandName[0], CultureInfo.InvariantCulture)}{commandName.Substring(1)}Command"; + return $"{char.ToUpper(commandName[0], CultureInfo.InvariantCulture)}{commandName[1..]}Command"; } + /// Gets the generated backing-field name for a command property. + /// The generated command property name. + /// The generated backing-field name. private static string GetGeneratedFieldName(string generatedCommandName) => - $"_{char.ToLower(generatedCommandName[0], CultureInfo.InvariantCulture)}{generatedCommandName.Substring(1)}"; + $"_{char.ToLower(generatedCommandName[0], CultureInfo.InvariantCulture)}{generatedCommandName[1..]}"; } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs index ce878173..87a5fbd7 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs @@ -1,28 +1,26 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveCommandGenerator : IIncrementalGenerator { /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => ctx.AddSource($"{AttributeDefinitions.ReactiveCommandAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ReactiveCommandAttribute, Encoding.UTF8))); // Gather info for all annotated command methods (starting from method declarations with at least one attribute) @@ -32,37 +30,41 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AttributeDefinitions.ReactiveCommandAttributeType, static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 }, static (context, token) => GetMethodInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) - .Collect(); + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .Collect() + .Combine(context.ReactiveUiIntegration()); // Generate the requested properties and methods context.RegisterSourceOutput(commandInfo, static (context, input) => { - var groupedcommandInfo = input.GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + List> groupedCommandInfo = []; - if (groupedcommandInfo.Length == 0) + foreach (var command in input.Left) { - return; - } - - foreach (var grouping in groupedcommandInfo) - { - var items = grouping.ToImmutableArray(); - - if (items.Length == 0) + var targetInfo = command.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedCommandInfo.TryGetValue(key, out var commands)) { - continue; + commands = []; + groupedCommandInfo.Add(key, commands); } - var (fileHintName, targetName, targetNamespace, targetVisibility, targetType) = grouping.Key; - - var source = GenerateSource(targetName, targetNamespace, targetVisibility, targetType, [.. grouping]); + commands.Add(command); + } - context.AddSource($"{fileHintName}.ReactiveCommands.g.cs", source); + foreach (var grouping in groupedCommandInfo) + { + var source = GenerateSource( + grouping.Key.TargetName, + grouping.Key.TargetNamespace, + grouping.Key.TargetVisibility, + grouping.Key.TargetType, + grouping.Value.ToArray(), + input.Right); + context.AddSource($"{grouping.Key.FileHintName}.ReactiveCommands.g.cs", source); } }); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/Models/ReactiveObjectInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/Models/ReactiveObjectInfo.cs index 81029968..c92228e6 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/Models/ReactiveObjectInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/Models/ReactiveObjectInfo.cs @@ -1,12 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model with gathered information about a generated ReactiveObject (view model). -/// +/// A model with gathered information about a generated ReactiveObject (view model). +/// The target type that owns the generated ReactiveObject members. internal sealed record ReactiveObjectInfo( TargetInfo TargetInfo); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.Execute.cs index b37472a1..39b9b36b 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.Execute.cs @@ -1,27 +1,29 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using ReactiveUI.SourceGenerators.Extensions; -using ReactiveUI.SourceGenerators.Helpers; using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactiveObject properties. -/// +/// A source generator for generating reactiveObject properties. public partial class ReactiveObjectGenerator { + /// Gets the generator type name used in generated-code metadata. internal static readonly string GeneratorName = typeof(ReactiveObjectGenerator).FullName!; + + /// Gets the generator assembly version used in generated-code metadata. internal static readonly string GeneratorVersion = typeof(ReactiveObjectGenerator).Assembly.GetName().Version.ToString(); + /// Creates metadata for a ReactiveObject-annotated class. + /// The attribute syntax context for the class. + /// The cancellation token for the generator operation. + /// The class metadata, or when not applicable. private static ReactiveObjectInfo? GetClassInfo(in GenericGeneratorAttributeSyntaxContext context, CancellationToken token) { if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) @@ -46,7 +48,19 @@ public partial class ReactiveObjectGenerator return new(targetInfo); } - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ReactiveObjectInfo reactiveObjectInfo) => + /// Generates the complete source document for a ReactiveObject declaration. + /// The name of the containing type. + /// The containing namespace. + /// The containing type visibility. + /// The containing type kind. + /// The selected ReactiveUI integration metadata. + /// The generated source document. + private static string GenerateSource( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + ReactiveUiIntegration integration) => $$""" // #pragma warning disable @@ -54,7 +68,7 @@ private static string GenerateSource(string containingTypeName, string containin using System; using System.Diagnostics.CodeAnalysis; using System.ComponentModel; -using ReactiveUI; +{{integration.UsingDirectives}} namespace {{containingNamespace}} { @@ -63,6 +77,39 @@ namespace {{containingNamespace}} /// {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IReactiveObject { +{{IndentGeneratedMembers(ReactiveObjectSource.Members)}} + } +} +#nullable restore +#pragma warning restore +"""; + + /// Indents reusable generated members within their containing type. + /// The generated members to indent. + /// The indented generated members. + private static string IndentGeneratedMembers(string members) + { + var builder = new StringBuilder(members.Length); + var atLineStart = true; + foreach (var character in members) + { + if (atLineStart && character is not '\r' and not '\n') + { + _ = builder.Append(" "); + } + + _ = builder.Append(character); + atLineStart = character == '\n'; + } + + return builder.ToString(); + } + + /// Contains reusable generated ReactiveObject source fragments. + private static class ReactiveObjectSource + { + /// Contains the generated ReactiveObject event implementation. + internal const string Members = """ private bool _propertyChangingEventsSubscribed; private bool _propertyChangedEventsSubscribed; @@ -111,9 +158,6 @@ void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => /// void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChangedHandler?.Invoke(this, args); + """; } } -#nullable restore -#pragma warning restore -"""; -} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.cs index d97988d3..d7276ad2 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.cs @@ -1,29 +1,27 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveObjectGenerator : IIncrementalGenerator { /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => - ctx.AddSource(AttributeDefinitions.ReactiveObjectAttributeType + ".g.cs", SourceText.From(AttributeDefinitions.ReactiveObjectAttribute, Encoding.UTF8))); + context.RegisterPostInitializationOutput(static ctx => + ctx.AddSource($"{AttributeDefinitions.ReactiveObjectAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ReactiveObjectAttribute, Encoding.UTF8))); // Gather info for all annotated IReactiveObject Classes var reactiveObjectInfo = @@ -32,29 +30,32 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AttributeDefinitions.ReactiveObjectAttributeType, static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, static (context, token) => GetClassInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) - .Collect(); + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .Collect() + .Combine(context.ReactiveUiIntegration()); // Generate the requested properties and methods for IReactiveObject context.RegisterSourceOutput(reactiveObjectInfo, static (context, input) => { - var groupedPropertyInfo = input.GroupBy( - static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), - static info => info) - .ToImmutableArray(); + Dictionary< + (string FileHintName, string TargetName, string TargetNamespace, string TargetVisibility, string TargetType), + ReactiveObjectInfo> groupedPropertyInfo = []; - foreach (var grouping in groupedPropertyInfo) + foreach (var reactiveObjectInfo in input.Left) { - var items = grouping.ToImmutableArray(); - - if (items.Length == 0) + var targetInfo = reactiveObjectInfo.TargetInfo; + var key = (targetInfo.FileHintName, targetInfo.TargetName, targetInfo.TargetNamespace, targetInfo.TargetVisibility, targetInfo.TargetType); + if (!groupedPropertyInfo.ContainsKey(key)) { - continue; + groupedPropertyInfo.Add(key, reactiveObjectInfo); } + } - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault()); - context.AddSource(grouping.Key.FileHintName + ".IReactiveObject.g.cs", source); + foreach (var grouping in groupedPropertyInfo) + { + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, input.Right); + context.AddSource($"{grouping.Key.FileHintName}.IReactiveObject.g.cs", source); } }); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/Models/RoutedControlHostInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/Models/RoutedControlHostInfo.cs index e5b4731c..075a3233 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/Models/RoutedControlHostInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/Models/RoutedControlHostInfo.cs @@ -1,15 +1,20 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model with gathered info on a given command method. -/// +/// Contains the metadata required to generate a routed control host. +/// The generated source file hint name. +/// The target type name. +/// The target namespace. +/// The target namespace declaration. +/// The target type visibility. +/// The target type keyword. +/// The routed control host base type name. +/// The attributes forwarded to the generated host. internal sealed record RoutedControlHostInfo( string FileHintName, string TargetName, diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.Execute.cs index c7abedd3..dbb4fc45 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.Execute.cs @@ -1,11 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Immutable; -using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -16,16 +15,20 @@ namespace ReactiveUI.SourceGenerators.WinForms; -/// -/// IViewForGenerator. -/// +/// Generates a Windows Forms routed control host. /// public partial class RoutedControlHostGenerator { + /// The fully qualified generator name written to generated-code attributes. private static readonly string GeneratorName = typeof(RoutedControlHostGenerator).FullName!; + + /// The generator assembly version written to generated-code attributes. private static readonly string GeneratorVersion = typeof(RoutedControlHostGenerator).Assembly.GetName().Version.ToString(); - private static RoutedControlHostInfo? GetClassInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) + /// Gets generation metadata for an attributed routed control host. + private static readonly RoutedControlHostInfoFactory GetClassInfo = static ( + in GeneratorAttributeSyntaxContext context, + CancellationToken token) => { if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) { @@ -42,8 +45,8 @@ public partial class RoutedControlHostGenerator token.ThrowIfCancellationRequested(); - var constructorArgument = attributeData.GetConstructorArguments().First(); - if (constructorArgument is not string baseTypeName) + var baseTypeName = GetFirstConstructorArgument(attributeData); + if (baseTypeName is null) { return default; } @@ -60,7 +63,13 @@ public partial class RoutedControlHostGenerator var compilation = context.SemanticModel.Compilation; var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); attributeData.GatherForwardedAttributesFromClass(semanticModel, declaredClass, token, out var attributesInfo); - var classAttributesInfo = attributesInfo.Select(x => x.ToString()).ToImmutableArray(); + var classAttributesBuilder = ImmutableArray.CreateBuilder(attributesInfo.Length); + foreach (var attributeInfo in attributesInfo) + { + classAttributesBuilder.Add(attributeInfo.ToString()); + } + + var classAttributesInfo = classAttributesBuilder.ToImmutable(); token.ThrowIfCancellationRequested(); @@ -78,13 +87,20 @@ public partial class RoutedControlHostGenerator targetInfo.TargetType, baseTypeName, classAttributesInfo); - } - - private static string GetRoutedControlHost(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, RoutedControlHostInfo vmcInfo, bool isNewerThan22) + }; + + /// Gets the routed control host source renderer. + private static readonly RoutedControlHostRenderer GetRoutedControlHost = static ( + containingTypeName, + containingNamespace, + containingClassVisibility, + containingType, + vmcInfo, + integration) => { // Prepare any forwarded property attributes - var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(vmcInfo.ForwardedAttributes)); - var exceptionHandler = isNewerThan22 + var forwardedAttributesString = GetForwardedAttributes(vmcInfo.ForwardedAttributes); + var exceptionHandler = integration.IsNewerThan22 ? "RxState.DefaultExceptionHandler!.OnNext" : "RxApp.DefaultExceptionHandler!.OnNext"; @@ -96,10 +112,10 @@ private static string GetRoutedControlHost(string containingTypeName, string con // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using ReactiveUI; +{{integration.UsingDirectives}} +using System; +using System.Collections.Generic; using System.ComponentModel; -using System.Reactive.Disposables; -using System.Reactive.Linq; using System.Windows.Forms; #pragma warning disable @@ -111,9 +127,10 @@ namespace {{containingNamespace}} [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : {{vmcInfo.BaseTypeName}}, IReactiveObject { - private readonly CompositeDisposable _disposables = []; + private readonly DisposableCollection _disposables = new(); private RoutingState? _router; private Control? _defaultContent; + private Control? _routedView; private IObservable? _viewContractObservable; /// @@ -122,50 +139,22 @@ namespace {{containingNamespace}} public {{containingTypeName}}() { InitializeComponent(); - _disposables.Add(this.WhenAny(x => x.DefaultContent, x => x.Value).Subscribe(x => + _disposables.Add(this.WhenAny(x => x.DefaultContent, x => x.Value).Subscribe(new ValueObserver(x => { if (x is not null && Controls.Count == 0) { Controls.Add(InitView(x)); components?.Add(DefaultContent); } - })); - ViewContractObservable = Observable.Return(default(string)!); - var vmAndContract = this.WhenAnyObservable(x => x.Router!.CurrentViewModel!).CombineLatest(this.WhenAnyObservable(x => x.ViewContractObservable!), (vm, contract) => new { ViewModel = vm, Contract = contract }); - Control? viewLastAdded = null; - _disposables.Add(vmAndContract.Subscribe(x => - { - // clear all hosted controls (view or default content) - SuspendLayout(); - Controls.Clear(); - viewLastAdded?.Dispose(); - if (x.ViewModel is null) - { - if (DefaultContent is not null) - { - InitView(DefaultContent); - Controls.Add(DefaultContent); - } - - ResumeLayout(); - return; - } - - var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(x.ViewModel, x.Contract); - if (view is not null) - { - view.ViewModel = x.ViewModel; - viewLastAdded = InitView((Control)view); - } - - if (viewLastAdded is not null) - { - Controls.Add(viewLastAdded); - } - - ResumeLayout(); - }, {{exceptionHandler}})); + }, {{exceptionHandler}}))); + ViewContractObservable = new ReturnObservable(default!); + var routeSubscription = new CombineLatestSubscription( + UpdateRoutedContent, + {{exceptionHandler}}); + _disposables.Add(routeSubscription); + routeSubscription.Connect( + this.WhenAnyObservable(x => x.Router!.CurrentViewModel!), + this.WhenAnyObservable(x => x.ViewContractObservable!)); } /// @@ -215,24 +204,406 @@ namespace {{containingNamespace}} /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { - if (disposing && components is not null) + if (disposing) { - components.Dispose(); _disposables.Dispose(); + _routedView?.Dispose(); + _routedView = null; + components?.Dispose(); } base.Dispose(disposing); } + private void UpdateRoutedContent(IRoutableViewModel? viewModel, string contract) + { + SuspendLayout(); + try + { + Controls.Clear(); + _routedView?.Dispose(); + _routedView = null; + + if (viewModel is null) + { + if (DefaultContent is not null) + { + Controls.Add(InitView(DefaultContent)); + } + + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(viewModel, contract); + if (view is not null) + { + view.ViewModel = viewModel; + _routedView = InitView((Control)view); + Controls.Add(_routedView); + } + } + finally + { + ResumeLayout(); + } + } + private static Control InitView(Control view) { view.Dock = DockStyle.Fill; return view; } + + private sealed class ReturnObservable(T value) : IObservable + { + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(value); + observer.OnCompleted(); + return EmptyDisposable.Instance; + } + } + + private sealed class ValueObserver(Action onNext, Action onError, Action? onCompleted = null) : IObserver + { + public void OnCompleted() => onCompleted?.Invoke(); + + public void OnError(Exception error) => onError(error); + + public void OnNext(T value) => onNext(value); + } + + private sealed class CombineLatestSubscription : IDisposable + { + private readonly object _gate = new(); + private readonly Action _onNext; + private readonly Action _onError; + private readonly SubscriptionSlot _leftSubscription = new(); + private readonly SubscriptionSlot _rightSubscription = new(); + private TLeft _left = default!; + private TRight _right = default!; + private bool _hasLeft; + private bool _hasRight; + private bool _leftCompleted; + private bool _rightCompleted; + private bool _isStopped; + + public CombineLatestSubscription( + Action onNext, + Action onError) + { + _onNext = onNext; + _onError = onError; + } + + public void Connect(IObservable left, IObservable right) + { + _leftSubscription.Set(left.Subscribe(new ValueObserver(SetLeft, Fail, CompleteLeft))); + if (IsStopped()) + { + return; + } + + _rightSubscription.Set(right.Subscribe(new ValueObserver(SetRight, Fail, CompleteRight))); + } + + public void Dispose() + { + lock (_gate) + { + _isStopped = true; + } + + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + + private void SetLeft(TLeft value) + { + lock (_gate) + { + if (_isStopped) + { + return; + } + + _left = value; + _hasLeft = true; + if (!_hasRight) + { + return; + } + + _onNext(value, _right); + } + } + + private void SetRight(TRight value) + { + lock (_gate) + { + if (_isStopped) + { + return; + } + + _right = value; + _hasRight = true; + if (!_hasLeft) + { + return; + } + + _onNext(_left, value); + } + } + + private void CompleteLeft() + { + var shouldStop = false; + lock (_gate) + { + if (_isStopped) + { + return; + } + + _leftCompleted = true; + shouldStop = !_hasLeft || _rightCompleted; + _isStopped = shouldStop; + } + + if (shouldStop) + { + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + } + + private void CompleteRight() + { + var shouldStop = false; + lock (_gate) + { + if (_isStopped) + { + return; + } + + _rightCompleted = true; + shouldStop = !_hasRight || _leftCompleted; + _isStopped = shouldStop; + } + + if (shouldStop) + { + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + } + + private void Fail(Exception error) + { + lock (_gate) + { + if (_isStopped) + { + return; + } + + _isStopped = true; + } + + try + { + _onError(error); + } + finally + { + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + } + + private bool IsStopped() + { + lock (_gate) + { + return _isStopped; + } + } + } + + private sealed class DisposableCollection : IDisposable + { + private readonly object _gate = new(); + private readonly List _items = new(); + private bool _isDisposed; + + public void Add(IDisposable item) + { + var disposeItem = false; + lock (_gate) + { + disposeItem = _isDisposed; + if (!disposeItem) + { + _items.Add(item); + } + } + + if (disposeItem) + { + item.Dispose(); + } + } + + public void Dispose() + { + IDisposable[] items; + lock (_gate) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + items = _items.ToArray(); + _items.Clear(); + } + + foreach (var item in items) + { + item.Dispose(); + } + } + } + + private sealed class SubscriptionSlot : IDisposable + { + private readonly object _gate = new(); + private IDisposable? _subscription; + private bool _isDisposed; + + public void Set(IDisposable subscription) + { + IDisposable? previous; + var disposeSubscription = false; + lock (_gate) + { + disposeSubscription = _isDisposed; + previous = _subscription; + if (!disposeSubscription) + { + _subscription = subscription; + } + } + + previous?.Dispose(); + if (disposeSubscription) + { + subscription.Dispose(); + } + } + + public void Dispose() + { + IDisposable? subscription; + lock (_gate) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + subscription = _subscription; + _subscription = null; + } + + subscription?.Dispose(); + } + } + + private sealed class EmptyDisposable : IDisposable + { + public static EmptyDisposable Instance { get; } = new(); + + public void Dispose() + { + } + } } } #nullable restore #pragma warning restore """; + }; + + /// Gets routed control host metadata from a generator attribute context. + /// The generator attribute context. + /// The cancellation token. + /// The routed control host metadata, or when the target is invalid. + private delegate RoutedControlHostInfo? RoutedControlHostInfoFactory( + in GeneratorAttributeSyntaxContext context, + CancellationToken token); + + /// Renders routed control host source from gathered metadata. + /// The containing type name. + /// The containing namespace. + /// The containing type visibility. + /// The containing type keyword. + /// The routed control host metadata. + /// The detected ReactiveUI integration. + /// The generated routed control host source. + private delegate string RoutedControlHostRenderer( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + RoutedControlHostInfo vmcInfo, + ReactiveUiIntegration integration); + + /// Gets the first string constructor argument from an attribute. + /// The attribute data. + /// The first string argument, or when none exists. + private static string? GetFirstConstructorArgument(AttributeData attributeData) + { + using var arguments = attributeData.GetConstructorArguments().GetEnumerator(); + return arguments.MoveNext() ? arguments.Current : null; + } + + /// Formats attributes forwarded to a generated host. + /// The forwarded attributes. + /// The formatted attributes. + private static string GetForwardedAttributes(EquatableArray forwardedAttributes) + { + var builder = new StringBuilder(); + foreach (var attribute in AttributeDefinitions.ExcludeFromCodeCoverage) + { + AppendAttribute(builder, attribute); + } + + foreach (var attribute in forwardedAttributes.AsImmutableArray()) + { + AppendAttribute(builder, attribute); + } + + return builder.ToString(); + } + + /// Appends an attribute to a generated attribute list. + /// The destination builder. + /// The attribute source. + private static void AppendAttribute(StringBuilder builder, string attribute) + { + if (builder.Length > 0) + { + _ = builder.Append("\n "); + } + + _ = builder.Append(attribute); } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs index 4c88c08f..6086a0f7 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs @@ -1,10 +1,7 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,19 +11,17 @@ namespace ReactiveUI.SourceGenerators.WinForms; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. [Generator(LanguageNames.CSharp)] public sealed partial class RoutedControlHostGenerator : IIncrementalGenerator { /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => ctx.AddSource($"{AttributeDefinitions.RoutedControlHostAttributeType}.g.cs", SourceText.From(AttributeDefinitions.GetRoutedControlHostAttribute(), Encoding.UTF8))); - var reactiveUiVersionProvider = context.ReactiveUIVersionIsGreaterThan22(); + var reactiveUiIntegrationProvider = context.ReactiveUiIntegration(); // Gather info for all annotated IViewFor Classes var rchInfo = @@ -35,35 +30,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AttributeDefinitions.RoutedControlHostAttributeType, static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, static (context, token) => GetClassInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) + .Where(static x => x is not null) + .Select(static (x, _) => x!) .Collect() - .Combine(reactiveUiVersionProvider); + .Combine(reactiveUiIntegrationProvider); // Generate the requested properties and methods for IViewFor context.RegisterSourceOutput(rchInfo, static (context, input) => { - var groupedPropertyInfo = input.Left.GroupBy( - static info => (info.FileHintName, info.TargetName, info.TargetNamespace, info.TargetVisibility, info.TargetType), - static info => info) - .ToImmutableArray(); - - if (groupedPropertyInfo.Length == 0) - { - return; - } - - foreach (var grouping in groupedPropertyInfo) + foreach (var info in input.Left) { - var items = grouping.ToImmutableArray(); - - if (items.Length == 0) - { - continue; - } - - var source = GetRoutedControlHost(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault(), input.Right); - context.AddSource($"{grouping.Key.FileHintName}.RoutedControlHost.g.cs", source); + var source = GetRoutedControlHost( + info.TargetName, + info.TargetNamespace, + info.TargetVisibility, + info.TargetType, + info, + input.Right); + context.AddSource($"{info.FileHintName}.RoutedControlHost.g.cs", source); } }); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/Models/ViewModelControlHostInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/Models/ViewModelControlHostInfo.cs index 9dd554fb..45220308 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/Models/ViewModelControlHostInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/Models/ViewModelControlHostInfo.cs @@ -1,15 +1,20 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Models; -/// -/// A model with gathered info on a given command method. -/// +/// Contains the metadata required to generate a view-model control host. +/// The generated source file hint name. +/// The target type name. +/// The target namespace. +/// The target namespace declaration. +/// The target type visibility. +/// The target type keyword. +/// The view-model control host base type name. +/// The attributes forwarded to the generated host. internal sealed record ViewModelControlHostInfo( string FileHintName, string TargetName, diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs index 61dbef24..7db1d101 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs @@ -1,11 +1,10 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Immutable; -using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -16,16 +15,20 @@ namespace ReactiveUI.SourceGenerators.WinForms; -/// -/// IViewForGenerator. -/// +/// Generates a Windows Forms view-model control host. /// public partial class ViewModelControlHostGenerator { + /// The fully qualified generator name written to generated-code attributes. private static readonly string GeneratorName = typeof(ViewModelControlHostGenerator).FullName!; + + /// The generator assembly version written to generated-code attributes. private static readonly string GeneratorVersion = typeof(ViewModelControlHostGenerator).Assembly.GetName().Version.ToString(); - private static ViewModelControlHostInfo? GetClassInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) + /// Gets generation metadata for an attributed view-model control host. + private static readonly ViewModelControlHostInfoFactory GetClassInfo = static ( + in GeneratorAttributeSyntaxContext context, + CancellationToken token) => { if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) { @@ -45,8 +48,8 @@ public partial class ViewModelControlHostGenerator return default; } - var constructorArgument = attributeData.GetConstructorArguments().First(); - if (constructorArgument is not string viewModelTypeName) + var viewModelTypeName = GetFirstConstructorArgument(attributeData); + if (viewModelTypeName is null) { return default; } @@ -55,7 +58,13 @@ public partial class ViewModelControlHostGenerator var compilation = context.SemanticModel.Compilation; var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); attributeData.GatherForwardedAttributesFromClass(semanticModel, declaredClass, token, out var attributesInfo); - var classAttributesInfo = attributesInfo.Select(x => x.ToString()).ToImmutableArray(); + var classAttributesBuilder = ImmutableArray.CreateBuilder(attributesInfo.Length); + foreach (var attributeInfo in attributesInfo) + { + classAttributesBuilder.Add(attributeInfo.ToString()); + } + + var classAttributesInfo = classAttributesBuilder.ToImmutable(); token.ThrowIfCancellationRequested(); @@ -71,13 +80,20 @@ public partial class ViewModelControlHostGenerator targetInfo.TargetType, viewModelTypeName!, classAttributesInfo); - } - - private static string GetViewModelControlHost(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ViewModelControlHostInfo vmcInfo, bool isNewerThan22) + }; + + /// Gets the view-model control host source renderer. + private static readonly ViewModelControlHostRenderer GetViewModelControlHost = static ( + containingTypeName, + containingNamespace, + containingClassVisibility, + containingType, + vmcInfo, + integration) => { // Prepare any forwarded property attributes - var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(vmcInfo.ForwardedAttributes)); - var exceptionHandler = isNewerThan22 + var forwardedAttributesString = GetForwardedAttributes(vmcInfo.ForwardedAttributes); + var exceptionHandler = integration.IsNewerThan22 ? "RxState.DefaultExceptionHandler!.OnNext" : "RxApp.DefaultExceptionHandler!.OnNext"; @@ -89,10 +105,10 @@ private static string GetViewModelControlHost(string containingTypeName, string // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using ReactiveUI; +{{integration.UsingDirectives}} +using System; +using System.Collections.Generic; using System.ComponentModel; -using System.Reactive.Disposables; -using System.Reactive.Linq; using System.Windows.Forms; #pragma warning disable @@ -104,7 +120,7 @@ namespace {{containingNamespace}} [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : {{vmcInfo.ViewModelTypeName}}, IReactiveObject, IViewFor { - private readonly CompositeDisposable _disposables = []; + private readonly DisposableCollection _disposables = new(); private Control? _defaultContent; private IObservable? _viewContractObservable; private object? _viewModel; @@ -118,10 +134,7 @@ namespace {{containingNamespace}} { InitializeComponent(); _cacheViews = DefaultCacheViewsEnabled; - foreach (var d in SetupBindings()) - { - _disposables.Add(d); - } + SetupBindings(); } /// @@ -197,19 +210,24 @@ namespace {{containingNamespace}} /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { - if (disposing && components is not null) + if (disposing) { - components.Dispose(); _disposables.Dispose(); + components?.Dispose(); } base.Dispose(disposing); } - private IEnumerable SetupBindings() + private void SetupBindings() { - var viewChanges = this.WhenAnyValue(x => x!.Content).WhereNotNull().OfType().Subscribe(x => + AddSubscription(this.WhenAnyValue(x => x!.Content), new ValueObserver(x => { + if (x is not Control control) + { + return; + } + // change the view in the ui SuspendLayout(); // clear out existing visible control view @@ -219,59 +237,418 @@ private IEnumerable SetupBindings() Controls.Remove(c); } - x!.Dock = DockStyle.Fill; - Controls.Add(x); + control.Dock = DockStyle.Fill; + Controls.Add(control); ResumeLayout(); - }); - yield return viewChanges!; - yield return this.WhenAnyValue(x => x.DefaultContent).Subscribe(x => + }, {{exceptionHandler}})); + AddSubscription(this.WhenAnyValue(x => x.DefaultContent), new ValueObserver(x => { if (x is not null) { Content = DefaultContent; } - }); - ViewContractObservable = Observable.Return(string.Empty); - var vmAndContract = this.WhenAnyValue(x => x.ViewModel).CombineLatest(this.WhenAnyObservable(x => x.ViewContractObservable!), (vm, contract) => new { ViewModel = vm, Contract = contract }); - yield return vmAndContract.Subscribe(x => + }, {{exceptionHandler}})); + ViewContractObservable = new ReturnObservable(string.Empty); + var viewModelSubscription = new CombineLatestSubscription( + UpdateContent, + {{exceptionHandler}}); + _disposables.Add(viewModelSubscription); + viewModelSubscription.Connect( + this.WhenAnyValue(x => x.ViewModel), + this.WhenAnyObservable(x => x.ViewContractObservable!)); + } + + private void AddSubscription(IObservable observable, IObserver observer) + { + var slot = new SubscriptionSlot(); + _disposables.Add(slot); + slot.Set(observable.Subscribe(observer)); + } + + private void UpdateContent(object? viewModel, string contract) + { + // set content to default when viewmodel is null + if (viewModel is null) + { + if (DefaultContent is not null) + { + Content = DefaultContent; + } + + return; + } + + if (CacheViews) + { + // when caching views, check the current viewmodel and type + var currentView = _content as IViewFor; + if (currentView?.ViewModel is not null && currentView.ViewModel.GetType() == viewModel.GetType()) + { + currentView.ViewModel = viewModel; + // return early here after setting the viewmodel + // allowing the view to update its bindings + return; + } + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(viewModel, contract); + if (view is not null) + { + view.ViewModel = viewModel; + Content = view; + } + } + + private sealed class ReturnObservable(T value) : IObservable + { + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(value); + observer.OnCompleted(); + return EmptyDisposable.Instance; + } + } + + private sealed class ValueObserver(Action onNext, Action onError, Action? onCompleted = null) : IObserver + { + public void OnCompleted() => onCompleted?.Invoke(); + + public void OnError(Exception error) => onError(error); + + public void OnNext(T value) => onNext(value); + } + + private sealed class CombineLatestSubscription : IDisposable + { + private readonly object _gate = new(); + private readonly Action _onNext; + private readonly Action _onError; + private readonly SubscriptionSlot _leftSubscription = new(); + private readonly SubscriptionSlot _rightSubscription = new(); + private TLeft _left = default!; + private TRight _right = default!; + private bool _hasLeft; + private bool _hasRight; + private bool _leftCompleted; + private bool _rightCompleted; + private bool _isStopped; + + public CombineLatestSubscription( + Action onNext, + Action onError) + { + _onNext = onNext; + _onError = onError; + } + + public void Connect(IObservable left, IObservable right) + { + _leftSubscription.Set(left.Subscribe(new ValueObserver(SetLeft, Fail, CompleteLeft))); + if (IsStopped()) + { + return; + } + + _rightSubscription.Set(right.Subscribe(new ValueObserver(SetRight, Fail, CompleteRight))); + } + + public void Dispose() + { + lock (_gate) + { + _isStopped = true; + } + + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + + private void SetLeft(TLeft value) { - // set content to default when viewmodel is null - if (ViewModel is null) + lock (_gate) { - if (DefaultContent is not null) + if (_isStopped) { - Content = DefaultContent; + return; } - return; + _left = value; + _hasLeft = true; + if (!_hasRight) + { + return; + } + + _onNext(value, _right); } + } - if (CacheViews) + private void SetRight(TRight value) + { + lock (_gate) { - // when caching views, check the current viewmodel and type - var c = _content as IViewFor; - if (c?.ViewModel is not null && c.ViewModel.GetType() == x.ViewModel!.GetType()) + if (_isStopped) { - c.ViewModel = x.ViewModel; - // return early here after setting the viewmodel - // allowing the view to update it's bindings return; } + + _right = value; + _hasRight = true; + if (!_hasLeft) + { + return; + } + + _onNext(_left, value); } + } - var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(x.ViewModel, x.Contract); - if (view is not null) + private void CompleteLeft() + { + var shouldStop = false; + lock (_gate) { - view.ViewModel = x.ViewModel; - Content = view; + if (_isStopped) + { + return; + } + + _leftCompleted = true; + shouldStop = !_hasLeft || _rightCompleted; + _isStopped = shouldStop; + } + + if (shouldStop) + { + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + } + + private void CompleteRight() + { + var shouldStop = false; + lock (_gate) + { + if (_isStopped) + { + return; + } + + _rightCompleted = true; + shouldStop = !_hasRight || _leftCompleted; + _isStopped = shouldStop; } - }, {{exceptionHandler}}); + + if (shouldStop) + { + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + } + + private void Fail(Exception error) + { + lock (_gate) + { + if (_isStopped) + { + return; + } + + _isStopped = true; + } + + try + { + _onError(error); + } + finally + { + _leftSubscription.Dispose(); + _rightSubscription.Dispose(); + } + } + + private bool IsStopped() + { + lock (_gate) + { + return _isStopped; + } + } + } + + private sealed class DisposableCollection : IDisposable + { + private readonly object _gate = new(); + private readonly List _items = new(); + private bool _isDisposed; + + public void Add(IDisposable item) + { + var disposeItem = false; + lock (_gate) + { + disposeItem = _isDisposed; + if (!disposeItem) + { + _items.Add(item); + } + } + + if (disposeItem) + { + item.Dispose(); + } + } + + public void Dispose() + { + IDisposable[] items; + lock (_gate) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + items = _items.ToArray(); + _items.Clear(); + } + + foreach (var item in items) + { + item.Dispose(); + } + } + } + + private sealed class SubscriptionSlot : IDisposable + { + private readonly object _gate = new(); + private IDisposable? _subscription; + private bool _isDisposed; + + public void Set(IDisposable subscription) + { + IDisposable? previous; + var disposeSubscription = false; + lock (_gate) + { + disposeSubscription = _isDisposed; + previous = _subscription; + if (!disposeSubscription) + { + _subscription = subscription; + } + } + + previous?.Dispose(); + if (disposeSubscription) + { + subscription.Dispose(); + } + } + + public void Dispose() + { + IDisposable? subscription; + lock (_gate) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + subscription = _subscription; + _subscription = null; + } + + subscription?.Dispose(); + } + } + + private sealed class EmptyDisposable : IDisposable + { + public static EmptyDisposable Instance { get; } = new(); + + public void Dispose() + { + } } } } #nullable restore #pragma warning restore """; + }; + + /// Gets view-model control host metadata from a generator attribute context. + /// The generator attribute context. + /// The cancellation token. + /// The view-model control host metadata, or when the target is invalid. + private delegate ViewModelControlHostInfo? ViewModelControlHostInfoFactory( + in GeneratorAttributeSyntaxContext context, + CancellationToken token); + + /// Renders view-model control host source from gathered metadata. + /// The containing type name. + /// The containing namespace. + /// The containing type visibility. + /// The containing type keyword. + /// The view-model control host metadata. + /// The detected ReactiveUI integration. + /// The generated view-model control host source. + private delegate string ViewModelControlHostRenderer( + string containingTypeName, + string containingNamespace, + string containingClassVisibility, + string containingType, + ViewModelControlHostInfo vmcInfo, + ReactiveUiIntegration integration); + + /// Gets the first string constructor argument from an attribute. + /// The attribute data. + /// The first string argument, or when none exists. + private static string? GetFirstConstructorArgument(AttributeData attributeData) + { + using var arguments = attributeData.GetConstructorArguments().GetEnumerator(); + return arguments.MoveNext() ? arguments.Current : null; + } + + /// Formats attributes forwarded to a generated host. + /// The forwarded attributes. + /// The formatted attributes. + private static string GetForwardedAttributes(EquatableArray forwardedAttributes) + { + var builder = new StringBuilder(); + foreach (var attribute in AttributeDefinitions.ExcludeFromCodeCoverage) + { + AppendAttribute(builder, attribute); + } + + foreach (var attribute in forwardedAttributes.AsImmutableArray()) + { + AppendAttribute(builder, attribute); + } + + return builder.ToString(); + } + + /// Appends an attribute to a generated attribute list. + /// The destination builder. + /// The attribute source. + private static void AppendAttribute(StringBuilder builder, string attribute) + { + if (builder.Length > 0) + { + _ = builder.Append("\n "); + } + + _ = builder.Append(attribute); } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs index 03413d54..5770bfaf 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs @@ -1,10 +1,7 @@ -// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved. -// Licensed to the ReactiveUI and contributors under one or more agreements. -// The ReactiveUI and contributors licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Immutable; -using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,19 +11,17 @@ namespace ReactiveUI.SourceGenerators.WinForms; -/// -/// A source generator for generating reactive properties. -/// +/// A source generator for generating reactive properties. [Generator(LanguageNames.CSharp)] public sealed partial class ViewModelControlHostGenerator : IIncrementalGenerator { /// public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => ctx.AddSource($"{AttributeDefinitions.ViewModelControlHostAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ViewModelControlHostAttribute, Encoding.UTF8))); - var reactiveUiVersionProvider = context.ReactiveUIVersionIsGreaterThan22(); + var reactiveUiIntegrationProvider = context.ReactiveUiIntegration(); // Gather info for all annotated IViewFor Classes var vmcInfo = @@ -35,35 +30,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AttributeDefinitions.ViewModelControlHostAttributeType, static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, static (context, token) => GetClassInfo(context, token)) - .Where(x => x != null) - .Select((x, _) => x!) + .Where(static x => x is not null) + .Select(static (x, _) => x!) .Collect() - .Combine(reactiveUiVersionProvider); + .Combine(reactiveUiIntegrationProvider); // Generate the requested properties and methods for IViewFor context.RegisterSourceOutput(vmcInfo, static (context, input) => { - var groupedPropertyInfo = input.Left.GroupBy( - static info => (info.FileHintName, info.TargetName, info.TargetNamespace, info.TargetVisibility, info.TargetType), - static info => info) - .ToImmutableArray(); - - if (groupedPropertyInfo.Length == 0) - { - return; - } - - foreach (var grouping in groupedPropertyInfo) + foreach (var info in input.Left) { - var items = grouping.ToImmutableArray(); - - if (items.Length == 0) - { - continue; - } - - var source = GetViewModelControlHost(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault(), input.Right); - context.AddSource($"{grouping.Key.FileHintName}.ViewModelControlHost.g.cs", source); + var source = GetViewModelControlHost( + info.TargetName, + info.TargetNamespace, + info.TargetVisibility, + info.TargetType, + info, + input.Right); + context.AddSource($"{info.FileHintName}.ViewModelControlHost.g.cs", source); } }); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn5000/ReactiveUI.SourceGenerators.Roslyn5000.csproj b/src/ReactiveUI.SourceGenerators.Roslyn5000/ReactiveUI.SourceGenerators.Roslyn5000.csproj index b8229523..e62e1a68 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn5000/ReactiveUI.SourceGenerators.Roslyn5000.csproj +++ b/src/ReactiveUI.SourceGenerators.Roslyn5000/ReactiveUI.SourceGenerators.Roslyn5000.csproj @@ -1,5 +1,4 @@ - - + $(RoslynTfm) enable @@ -11,12 +10,10 @@ false true false - A MVVM framework that integrates with the Reactive Extensions for .NET to create elegant, testable User Interfaces that run on any mobile or desktop platform. This is the Source Generators package for ReactiveUI ReactiveUI.SourceGenerators $(DefineConstants);ROSYLN_500 - @@ -24,18 +21,14 @@ - - - - diff --git a/src/ReactiveUI.SourceGenerators.slnx b/src/ReactiveUI.SourceGenerators.slnx index 49d7301d..935ff8c8 100644 --- a/src/ReactiveUI.SourceGenerators.slnx +++ b/src/ReactiveUI.SourceGenerators.slnx @@ -8,7 +8,6 @@ - diff --git a/src/TestApps/TestAvaloniaApplication.Desktop/Program.cs b/src/TestApps/TestAvaloniaApplication.Desktop/Program.cs index bf3e9883..5baa2d10 100644 --- a/src/TestApps/TestAvaloniaApplication.Desktop/Program.cs +++ b/src/TestApps/TestAvaloniaApplication.Desktop/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestAvaloniaApplication/App.axaml.cs b/src/TestApps/TestAvaloniaApplication/App.axaml.cs index 7b3403ba..bbf4edc0 100644 --- a/src/TestApps/TestAvaloniaApplication/App.axaml.cs +++ b/src/TestApps/TestAvaloniaApplication/App.axaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestAvaloniaApplication/ViewModels/MainViewModel.cs b/src/TestApps/TestAvaloniaApplication/ViewModels/MainViewModel.cs index bc7a7e0b..cb9aa653 100644 --- a/src/TestApps/TestAvaloniaApplication/ViewModels/MainViewModel.cs +++ b/src/TestApps/TestAvaloniaApplication/ViewModels/MainViewModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestAvaloniaApplication/ViewModels/ViewModelBase.cs b/src/TestApps/TestAvaloniaApplication/ViewModels/ViewModelBase.cs index dd371b74..c321507c 100644 --- a/src/TestApps/TestAvaloniaApplication/ViewModels/ViewModelBase.cs +++ b/src/TestApps/TestAvaloniaApplication/ViewModels/ViewModelBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestAvaloniaApplication/Views/MainView.axaml.cs b/src/TestApps/TestAvaloniaApplication/Views/MainView.axaml.cs index 2042338c..d9d5087a 100644 --- a/src/TestApps/TestAvaloniaApplication/Views/MainView.axaml.cs +++ b/src/TestApps/TestAvaloniaApplication/Views/MainView.axaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestAvaloniaApplication/Views/MainWindow.axaml.cs b/src/TestApps/TestAvaloniaApplication/Views/MainWindow.axaml.cs index 3f81182f..85e6e4f6 100644 --- a/src/TestApps/TestAvaloniaApplication/Views/MainWindow.axaml.cs +++ b/src/TestApps/TestAvaloniaApplication/Views/MainWindow.axaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/App.xaml.cs b/src/TestApps/TestMauiApplication/App.xaml.cs index 79782f9f..66fe9c67 100644 --- a/src/TestApps/TestMauiApplication/App.xaml.cs +++ b/src/TestApps/TestMauiApplication/App.xaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/AppShell.xaml.cs b/src/TestApps/TestMauiApplication/AppShell.xaml.cs index f1159141..e671534c 100644 --- a/src/TestApps/TestMauiApplication/AppShell.xaml.cs +++ b/src/TestApps/TestMauiApplication/AppShell.xaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/MainPage.xaml.cs b/src/TestApps/TestMauiApplication/MainPage.xaml.cs index 36d7f47a..15f7ad00 100644 --- a/src/TestApps/TestMauiApplication/MainPage.xaml.cs +++ b/src/TestApps/TestMauiApplication/MainPage.xaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/MainViewModel.cs b/src/TestApps/TestMauiApplication/MainViewModel.cs index 0d77f5f1..60590b57 100644 --- a/src/TestApps/TestMauiApplication/MainViewModel.cs +++ b/src/TestApps/TestMauiApplication/MainViewModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/MauiProgram.cs b/src/TestApps/TestMauiApplication/MauiProgram.cs index ee5bb643..c9b560d1 100644 --- a/src/TestApps/TestMauiApplication/MauiProgram.cs +++ b/src/TestApps/TestMauiApplication/MauiProgram.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/Platforms/Android/MainActivity.cs b/src/TestApps/TestMauiApplication/Platforms/Android/MainActivity.cs index 67b44bf5..3d11a827 100644 --- a/src/TestApps/TestMauiApplication/Platforms/Android/MainActivity.cs +++ b/src/TestApps/TestMauiApplication/Platforms/Android/MainActivity.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/Platforms/Android/MainApplication.cs b/src/TestApps/TestMauiApplication/Platforms/Android/MainApplication.cs index 69155d06..f61e6738 100644 --- a/src/TestApps/TestMauiApplication/Platforms/Android/MainApplication.cs +++ b/src/TestApps/TestMauiApplication/Platforms/Android/MainApplication.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/AppDelegate.cs b/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/AppDelegate.cs index 574d40a6..8f28ee04 100644 --- a/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/AppDelegate.cs +++ b/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/AppDelegate.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/Program.cs b/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/Program.cs index 888d1fe9..86f7bf12 100644 --- a/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/Program.cs +++ b/src/TestApps/TestMauiApplication/Platforms/MacCatalyst/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/Platforms/Windows/App.xaml.cs b/src/TestApps/TestMauiApplication/Platforms/Windows/App.xaml.cs index c5da9436..1517aad2 100644 --- a/src/TestApps/TestMauiApplication/Platforms/Windows/App.xaml.cs +++ b/src/TestApps/TestMauiApplication/Platforms/Windows/App.xaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/Platforms/iOS/AppDelegate.cs b/src/TestApps/TestMauiApplication/Platforms/iOS/AppDelegate.cs index 574d40a6..8f28ee04 100644 --- a/src/TestApps/TestMauiApplication/Platforms/iOS/AppDelegate.cs +++ b/src/TestApps/TestMauiApplication/Platforms/iOS/AppDelegate.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestMauiApplication/Platforms/iOS/Program.cs b/src/TestApps/TestMauiApplication/Platforms/iOS/Program.cs index 888d1fe9..86f7bf12 100644 --- a/src/TestApps/TestMauiApplication/Platforms/iOS/Program.cs +++ b/src/TestApps/TestMauiApplication/Platforms/iOS/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. diff --git a/src/TestApps/TestWinFormsApplication/BasePackageRoutedHost.Designer.cs b/src/TestApps/TestWinFormsApplication/BasePackageRoutedHost.Designer.cs new file mode 100644 index 00000000..730fd76f --- /dev/null +++ b/src/TestApps/TestWinFormsApplication/BasePackageRoutedHost.Designer.cs @@ -0,0 +1,12 @@ +namespace TestWinFormsApplication; + +partial class BasePackageRoutedHost +{ + private System.ComponentModel.IContainer components = null!; + + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + } +} diff --git a/src/TestApps/TestWinFormsApplication/BasePackageRoutedHost.cs b/src/TestApps/TestWinFormsApplication/BasePackageRoutedHost.cs new file mode 100644 index 00000000..c431f67b --- /dev/null +++ b/src/TestApps/TestWinFormsApplication/BasePackageRoutedHost.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.SourceGenerators.WinForms; + +namespace TestWinFormsApplication; + +/// Verifies the routed host generator against the ReactiveUI base package. +[RoutedControlHost(nameof(UserControl))] +public partial class BasePackageRoutedHost +{ + /// Gets a value indicating whether this host uses the base ReactiveUI package. + internal static bool UsesBaseReactiveUiPackage => true; +} diff --git a/src/TestApps/TestWinFormsApplication/BasePackageViewModelHost.Designer.cs b/src/TestApps/TestWinFormsApplication/BasePackageViewModelHost.Designer.cs new file mode 100644 index 00000000..55fa9a46 --- /dev/null +++ b/src/TestApps/TestWinFormsApplication/BasePackageViewModelHost.Designer.cs @@ -0,0 +1,12 @@ +namespace TestWinFormsApplication; + +partial class BasePackageViewModelHost +{ + private System.ComponentModel.IContainer components = null!; + + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + } +} diff --git a/src/TestApps/TestWinFormsApplication/BasePackageViewModelHost.cs b/src/TestApps/TestWinFormsApplication/BasePackageViewModelHost.cs new file mode 100644 index 00000000..7772dceb --- /dev/null +++ b/src/TestApps/TestWinFormsApplication/BasePackageViewModelHost.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.SourceGenerators.WinForms; + +namespace TestWinFormsApplication; + +/// Verifies the view-model host generator against the ReactiveUI base package. +[ViewModelControlHost(nameof(UserControl))] +public partial class BasePackageViewModelHost +{ + /// Gets a value indicating whether this host uses the base ReactiveUI package. + internal static bool UsesBaseReactiveUiPackage => true; +} diff --git a/src/TestApps/TestWinFormsApplication/Form1.Designer.cs b/src/TestApps/TestWinFormsApplication/Form1.Designer.cs index addb20f5..37f52964 100644 --- a/src/TestApps/TestWinFormsApplication/Form1.Designer.cs +++ b/src/TestApps/TestWinFormsApplication/Form1.Designer.cs @@ -1,4 +1,4 @@ -namespace WinFormsApp1 +namespace WinFormsApp1 { [ReactiveUI.SourceGenerators.IViewFor] partial class Form1 diff --git a/src/TestApps/TestWinFormsApplication/Form1.cs b/src/TestApps/TestWinFormsApplication/Form1.cs index 7556c033..c123c6c3 100644 --- a/src/TestApps/TestWinFormsApplication/Form1.cs +++ b/src/TestApps/TestWinFormsApplication/Form1.cs @@ -1,23 +1,17 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -namespace WinFormsApp1 +namespace WinFormsApp1; + +/// Represents the primary form for the Windows Forms sample application. +/// +public partial class Form1 : Form { - /// - /// Form1. - /// - /// - public partial class Form1 : Form + /// Initializes a new instance of the class. + public Form1() { - /// - /// Initializes a new instance of the class. - /// - public Form1() - { - InitializeComponent(); - ViewModel = new MainViewModel(); - } + InitializeComponent(); + ViewModel = new(); } } diff --git a/src/TestApps/TestWinFormsApplication/MainViewModel.cs b/src/TestApps/TestWinFormsApplication/MainViewModel.cs index 12786767..b03379ae 100644 --- a/src/TestApps/TestWinFormsApplication/MainViewModel.cs +++ b/src/TestApps/TestWinFormsApplication/MainViewModel.cs @@ -1,15 +1,11 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI; -namespace WinFormsApp1 -{ - /// - /// MainViewModel. - /// - /// - public class MainViewModel : ReactiveObject; -} +namespace WinFormsApp1; + +/// Represents the primary view model for the Windows Forms sample application. +/// +public class MainViewModel : ReactiveObject; diff --git a/src/TestApps/TestWinFormsApplication/Program.cs b/src/TestApps/TestWinFormsApplication/Program.cs index 9a8977e9..729c0410 100644 --- a/src/TestApps/TestWinFormsApplication/Program.cs +++ b/src/TestApps/TestWinFormsApplication/Program.cs @@ -1,22 +1,19 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -namespace WinFormsApp1 +namespace WinFormsApp1; + +/// Provides the entry point for the Windows Forms sample application. +internal static class Program { - internal static class Program + /// Starts the Windows Forms sample application. + [STAThread] + private static void Main() { - /// - /// The main entry point for the application. - /// - [STAThread] - private static void Main() - { - // To customize application configuration such as set high DPI settings or default font, - // see https://aka.ms/applicationconfiguration. - ApplicationConfiguration.Initialize(); - Application.Run(new Form1()); - } + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new Form1()); } } diff --git a/src/TestApps/TestWinFormsApplication/TestWinFormsApplication.csproj b/src/TestApps/TestWinFormsApplication/TestWinFormsApplication.csproj index b2389c15..68059003 100644 --- a/src/TestApps/TestWinFormsApplication/TestWinFormsApplication.csproj +++ b/src/TestApps/TestWinFormsApplication/TestWinFormsApplication.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -14,7 +14,7 @@ - + - \ No newline at end of file + diff --git a/src/TestApps/TestWpfApplication/App.xaml.cs b/src/TestApps/TestWpfApplication/App.xaml.cs index 11ed1615..5fae58c3 100644 --- a/src/TestApps/TestWpfApplication/App.xaml.cs +++ b/src/TestApps/TestWpfApplication/App.xaml.cs @@ -1,14 +1,10 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows; -namespace WpfApp1 -{ - /// - /// Interaction logic for App.xaml. - /// - public partial class App : Application; -} +namespace WpfApp1; + +/// Provides the interaction logic for App.xaml. +public partial class App : Application; diff --git a/src/TestApps/TestWpfApplication/AssemblyInfo.cs b/src/TestApps/TestWpfApplication/AssemblyInfo.cs index 65eca803..72456726 100644 --- a/src/TestApps/TestWpfApplication/AssemblyInfo.cs +++ b/src/TestApps/TestWpfApplication/AssemblyInfo.cs @@ -1,6 +1,5 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows; diff --git a/src/TestApps/TestWpfApplication/MainViewModel.cs b/src/TestApps/TestWpfApplication/MainViewModel.cs index 2f1486e3..575ab2a8 100644 --- a/src/TestApps/TestWpfApplication/MainViewModel.cs +++ b/src/TestApps/TestWpfApplication/MainViewModel.cs @@ -1,14 +1,24 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveUI; +using ReactiveUI.Primitives.Concurrency; +using ReactiveUI.SourceGenerators; -namespace WpfApp1 +namespace WpfApp1; + +/// Represents the primary view model for the WPF sample application. +public partial class MainViewModel : ReactiveObject { - /// - /// MainViewModel. - /// - public class MainViewModel : ReactiveObject; + /// Provides the scheduler used by the generated command. + private readonly ISequencer _scheduler = RxSchedulers.MainThreadScheduler; + + /// Stores the editable display name. + [Reactive] + private string? _name; + + /// Saves the current display name. + [ReactiveCommand(OutputScheduler = nameof(_scheduler))] + private void Save() => Name = Name?.Trim(); } diff --git a/src/TestApps/TestWpfApplication/MainWindow.xaml.cs b/src/TestApps/TestWpfApplication/MainWindow.xaml.cs index 42287315..e224425c 100644 --- a/src/TestApps/TestWpfApplication/MainWindow.xaml.cs +++ b/src/TestApps/TestWpfApplication/MainWindow.xaml.cs @@ -1,25 +1,20 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows; -namespace WpfApp1 +namespace WpfApp1; + +/// Provides the interaction logic for MainWindow.xaml. +[ReactiveUI.SourceGenerators.IViewFor] +public partial class MainWindow : Window { - /// - /// Interaction logic for MainWindow.xaml. - /// - [ReactiveUI.SourceGenerators.IViewFor] - public partial class MainWindow : Window + /// Initializes a new instance of the class. + public MainWindow() { - /// - /// Initializes a new instance of the class. - /// - public MainWindow() - { - InitializeComponent(); - DataContext = ViewModel = new MainViewModel(); - } + InitializeComponent(); + ViewModel = new(); + DataContext = ViewModel; } } diff --git a/src/TestApps/TestWpfApplication/TestWpfApplication.csproj b/src/TestApps/TestWpfApplication/TestWpfApplication.csproj index eb2b8b61..6cefc1c7 100644 --- a/src/TestApps/TestWpfApplication/TestWpfApplication.csproj +++ b/src/TestApps/TestWpfApplication/TestWpfApplication.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -14,7 +14,7 @@ - + diff --git a/src/stylecop.json b/src/stylecop.json deleted file mode 100644 index 7597479f..00000000 --- a/src/stylecop.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", - "settings": { - "indentation": { - "useTabs": false, - "indentationSize": 4 - }, - "documentationRules": { - "documentExposedElements": true, - "documentInternalElements": false, - "documentPrivateElements": false, - "documentInterfaces": true, - "documentPrivateFields": false, - "documentationCulture": "en-US", - "companyName": "ReactiveUI and contributors", - "copyrightText": "Copyright (c) 2026 {companyName}. All rights reserved.\nLicensed to the {companyName} under one or more agreements.\nThe {companyName} licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.", - "variables": { - "licenseName": "MIT", - "licenseFile": "LICENSE" - }, - "xmlHeader": false - }, - "layoutRules": { - "newlineAtEndOfFile": "allow", - "allowConsecutiveUsings": true - }, - "maintainabilityRules": { - "topLevelTypes": [ - "class", - "interface", - "struct", - "enum", - "delegate" - ] - }, - "orderingRules": { - "usingDirectivesPlacement": "outsideNamespace", - "systemUsingDirectivesFirst": true - } - } -}