Skip to content

Commit feade75

Browse files
authored
Add option for analyzer RCS1146 (#1688)
1 parent 94108cd commit feade75

File tree

16 files changed

+161
-57
lines changed

16 files changed

+161
-57
lines changed

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add option `roslynator_null_conditional_operator.avoid_negative_boolean_comparison` ([PR](https://github.com/dotnet/roslynator/pull/1688))
13+
- Do not suggest to use null-conditional operator when result would be `... != true/false`
14+
- Applicable for [RCS1146](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1146)
15+
1016
### Fixed
1117

1218
- Fix analyzer [RCS1172](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1172) ([PR](https://github.com/dotnet/roslynator/pull/1710))

src/Analyzers.CodeFixes/CSharp/CodeFixes/UseConditionalAccessCodeFixProvider.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ private static async Task<Document> UseConditionalAccessAsync(
7979
{
8080
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
8181

82-
SyntaxKind kind = binaryExpression.Kind();
82+
SyntaxKind binaryExpressionKind = binaryExpression.Kind();
8383

84-
(ExpressionSyntax left, ExpressionSyntax right) = UseConditionalAccessAnalyzer.GetFixableExpressions(binaryExpression, kind, semanticModel, cancellationToken);
84+
(ExpressionSyntax left, ExpressionSyntax right) = UseConditionalAccessAnalyzer.GetFixableExpressions(binaryExpression, binaryExpressionKind, semanticModel, cancellationToken);
8585

86-
NullCheckStyles allowedStyles = (kind == SyntaxKind.LogicalAndExpression)
86+
NullCheckStyles allowedStyles = (binaryExpressionKind == SyntaxKind.LogicalAndExpression)
8787
? (NullCheckStyles.NotEqualsToNull | NullCheckStyles.IsNotNull)
8888
: (NullCheckStyles.EqualsToNull | NullCheckStyles.IsNull);
8989

@@ -134,12 +134,12 @@ private static async Task<Document> UseConditionalAccessAsync(
134134
}
135135
case SyntaxKind.LogicalNotExpression:
136136
{
137-
builder.Append((kind == SyntaxKind.LogicalAndExpression) ? " == false" : " != true");
137+
builder.Append((binaryExpressionKind == SyntaxKind.LogicalAndExpression) ? " == false" : " != true");
138138
break;
139139
}
140140
default:
141141
{
142-
builder.Append((kind == SyntaxKind.LogicalAndExpression) ? " == true" : " != false");
142+
builder.Append((binaryExpressionKind == SyntaxKind.LogicalAndExpression) ? " == true" : " != false");
143143
break;
144144
}
145145
}

src/Analyzers.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4484,6 +4484,9 @@ string s2 = s as string;]]></Before>
44844484
<DefaultSeverity>Info</DefaultSeverity>
44854485
<IsEnabledByDefault>true</IsEnabledByDefault>
44864486
<MinLanguageVersion>6.0</MinLanguageVersion>
4487+
<ConfigOptions>
4488+
<Option Key="null_conditional_operator.avoid_negative_boolean_comparison" />
4489+
</ConfigOptions>
44874490
<Samples>
44884491
<Sample>
44894492
<Before><![CDATA[if (s != null && s.StartsWith("a"))

src/Analyzers/CSharp/Analysis/UseConditionalAccessAnalyzer.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ private static void AnalyzeBinaryExpression(SyntaxNodeAnalysisContext context)
126126
if (left is null)
127127
return;
128128

129+
if (context.GetConfigOptions().AvoidNegativeBooleanComparison()
130+
&& WillBeConvertedToNegativeBooleanComparison(binaryExpression, right))
131+
{
132+
return;
133+
}
134+
129135
ISymbol operatorSymbol = context.SemanticModel.GetSymbol(binaryExpression, context.CancellationToken);
130136

131137
if (operatorSymbol?.Name == WellKnownMemberNames.BitwiseOrOperatorName)
@@ -456,4 +462,32 @@ bool IsFirstChild(SyntaxNode node)
456462
return true;
457463
}
458464
}
465+
466+
private static bool WillBeConvertedToNegativeBooleanComparison(ExpressionSyntax binaryExpression, ExpressionSyntax rightExpression)
467+
{
468+
switch (rightExpression.Kind())
469+
{
470+
case SyntaxKind.LogicalOrExpression:
471+
case SyntaxKind.LogicalAndExpression:
472+
case SyntaxKind.BitwiseOrExpression:
473+
case SyntaxKind.BitwiseAndExpression:
474+
case SyntaxKind.ExclusiveOrExpression:
475+
case SyntaxKind.EqualsExpression:
476+
case SyntaxKind.NotEqualsExpression:
477+
case SyntaxKind.LessThanExpression:
478+
case SyntaxKind.LessThanOrEqualExpression:
479+
case SyntaxKind.GreaterThanExpression:
480+
case SyntaxKind.GreaterThanOrEqualExpression:
481+
case SyntaxKind.IsExpression:
482+
case SyntaxKind.AsExpression:
483+
case SyntaxKind.IsPatternExpression:
484+
{
485+
return false;
486+
}
487+
default:
488+
{
489+
return binaryExpression.IsKind(SyntaxKind.LogicalOrExpression);
490+
}
491+
}
492+
}
459493
}

src/Common/CSharp/Extensions/CodeStyleExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,4 +621,9 @@ private static bool TryGetNewLinePosition(
621621
newLinePosition = NewLinePosition.None;
622622
return false;
623623
}
624+
625+
public static bool AvoidNegativeBooleanComparison(this AnalyzerConfigOptions configOptions)
626+
{
627+
return ConfigOptions.TryGetValueAsBool(configOptions, ConfigOptions.NullConditionalOperator_AvoidNegativeBooleanComparison, out bool value) && value;
628+
}
624629
}

src/Common/ConfigOptionKeys.Generated.cs

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,45 @@ namespace Roslynator
66
{
77
internal static partial class ConfigOptionKeys
88
{
9-
public const string AccessibilityModifiers = "roslynator_accessibility_modifiers";
10-
public const string AccessorBracesStyle = "roslynator_accessor_braces_style";
11-
public const string ArrayCreationTypeStyle = "roslynator_array_creation_type_style";
12-
public const string ArrowTokenNewLine = "roslynator_arrow_token_new_line";
13-
public const string BinaryOperatorNewLine = "roslynator_binary_operator_new_line";
14-
public const string BlankLineAfterFileScopedNamespaceDeclaration = "roslynator_blank_line_after_file_scoped_namespace_declaration";
15-
public const string BlankLineBetweenClosingBraceAndSwitchSection = "roslynator_blank_line_between_closing_brace_and_switch_section";
16-
public const string BlankLineBetweenSingleLineAccessors = "roslynator_blank_line_between_single_line_accessors";
17-
public const string BlankLineBetweenSwitchSections = "roslynator_blank_line_between_switch_sections";
18-
public const string BlankLineBetweenUsingDirectives = "roslynator_blank_line_between_using_directives";
19-
public const string BlockBracesStyle = "roslynator_block_braces_style";
20-
public const string BodyStyle = "roslynator_body_style";
21-
public const string ConditionalOperatorConditionParenthesesStyle = "roslynator_conditional_operator_condition_parentheses_style";
22-
public const string ConditionalOperatorNewLine = "roslynator_conditional_operator_new_line";
23-
public const string ConfigureAwait = "roslynator_configure_await";
24-
public const string DocCommentSummaryStyle = "roslynator_doc_comment_summary_style";
25-
public const string EmptyStringStyle = "roslynator_empty_string_style";
26-
public const string EnumFlagValueStyle = "roslynator_enum_flag_value_style";
27-
public const string EnumHasFlagStyle = "roslynator_enum_has_flag_style";
28-
public const string EqualsTokenNewLine = "roslynator_equals_token_new_line";
29-
public const string InfiniteLoopStyle = "roslynator_infinite_loop_style";
30-
public const string MaxLineLength = "roslynator_max_line_length";
31-
public const string NewLineAtEndOfFile = "roslynator_new_line_at_end_of_file";
32-
public const string NewLineBeforeWhileInDoStatement = "roslynator_new_line_before_while_in_do_statement";
33-
public const string NullCheckStyle = "roslynator_null_check_style";
34-
public const string NullConditionalOperatorNewLine = "roslynator_null_conditional_operator_new_line";
35-
public const string ObjectCreationParenthesesStyle = "roslynator_object_creation_parentheses_style";
36-
public const string ObjectCreationTypeStyle = "roslynator_object_creation_type_style";
37-
public const string PrefixFieldIdentifierWithUnderscore = "roslynator_prefix_field_identifier_with_underscore";
38-
public const string SuppressUnityScriptMethods = "roslynator_suppress_unity_script_methods";
39-
public const string TabLength = "roslynator_tab_length";
40-
public const string TrailingCommaStyle = "roslynator_trailing_comma_style";
41-
public const string UnityCodeAnalysisEnabled = "roslynator_unity_code_analysis.enabled";
42-
public const string UseAnonymousFunctionOrMethodGroup = "roslynator_use_anonymous_function_or_method_group";
43-
public const string UseBlockBodyWhenDeclarationSpansOverMultipleLines = "roslynator_use_block_body_when_declaration_spans_over_multiple_lines";
44-
public const string UseBlockBodyWhenExpressionSpansOverMultipleLines = "roslynator_use_block_body_when_expression_spans_over_multiple_lines";
45-
public const string UseCollectionExpression = "roslynator_use_collection_expression";
46-
public const string UseVar = "roslynator_use_var";
47-
public const string UseVarInsteadOfImplicitObjectCreation = "roslynator_use_var_instead_of_implicit_object_creation";
9+
public const string AccessibilityModifiers = "roslynator_accessibility_modifiers";
10+
public const string AccessorBracesStyle = "roslynator_accessor_braces_style";
11+
public const string ArrayCreationTypeStyle = "roslynator_array_creation_type_style";
12+
public const string ArrowTokenNewLine = "roslynator_arrow_token_new_line";
13+
public const string BinaryOperatorNewLine = "roslynator_binary_operator_new_line";
14+
public const string BlankLineAfterFileScopedNamespaceDeclaration = "roslynator_blank_line_after_file_scoped_namespace_declaration";
15+
public const string BlankLineBetweenClosingBraceAndSwitchSection = "roslynator_blank_line_between_closing_brace_and_switch_section";
16+
public const string BlankLineBetweenSingleLineAccessors = "roslynator_blank_line_between_single_line_accessors";
17+
public const string BlankLineBetweenSwitchSections = "roslynator_blank_line_between_switch_sections";
18+
public const string BlankLineBetweenUsingDirectives = "roslynator_blank_line_between_using_directives";
19+
public const string BlockBracesStyle = "roslynator_block_braces_style";
20+
public const string BodyStyle = "roslynator_body_style";
21+
public const string ConditionalOperatorConditionParenthesesStyle = "roslynator_conditional_operator_condition_parentheses_style";
22+
public const string ConditionalOperatorNewLine = "roslynator_conditional_operator_new_line";
23+
public const string ConfigureAwait = "roslynator_configure_await";
24+
public const string DocCommentSummaryStyle = "roslynator_doc_comment_summary_style";
25+
public const string EmptyStringStyle = "roslynator_empty_string_style";
26+
public const string EnumFlagValueStyle = "roslynator_enum_flag_value_style";
27+
public const string EnumHasFlagStyle = "roslynator_enum_has_flag_style";
28+
public const string EqualsTokenNewLine = "roslynator_equals_token_new_line";
29+
public const string InfiniteLoopStyle = "roslynator_infinite_loop_style";
30+
public const string MaxLineLength = "roslynator_max_line_length";
31+
public const string NewLineAtEndOfFile = "roslynator_new_line_at_end_of_file";
32+
public const string NewLineBeforeWhileInDoStatement = "roslynator_new_line_before_while_in_do_statement";
33+
public const string NullCheckStyle = "roslynator_null_check_style";
34+
public const string NullConditionalOperator_AvoidNegativeBooleanComparison = "roslynator_null_conditional_operator.avoid_negative_boolean_comparison";
35+
public const string NullConditionalOperatorNewLine = "roslynator_null_conditional_operator_new_line";
36+
public const string ObjectCreationParenthesesStyle = "roslynator_object_creation_parentheses_style";
37+
public const string ObjectCreationTypeStyle = "roslynator_object_creation_type_style";
38+
public const string PrefixFieldIdentifierWithUnderscore = "roslynator_prefix_field_identifier_with_underscore";
39+
public const string SuppressUnityScriptMethods = "roslynator_suppress_unity_script_methods";
40+
public const string TabLength = "roslynator_tab_length";
41+
public const string TrailingCommaStyle = "roslynator_trailing_comma_style";
42+
public const string UnityCodeAnalysisEnabled = "roslynator_unity_code_analysis.enabled";
43+
public const string UseAnonymousFunctionOrMethodGroup = "roslynator_use_anonymous_function_or_method_group";
44+
public const string UseBlockBodyWhenDeclarationSpansOverMultipleLines = "roslynator_use_block_body_when_declaration_spans_over_multiple_lines";
45+
public const string UseBlockBodyWhenExpressionSpansOverMultipleLines = "roslynator_use_block_body_when_expression_spans_over_multiple_lines";
46+
public const string UseCollectionExpression = "roslynator_use_collection_expression";
47+
public const string UseVar = "roslynator_use_var";
48+
public const string UseVarInsteadOfImplicitObjectCreation = "roslynator_use_var_instead_of_implicit_object_creation";
4849
}
4950
}

src/Common/ConfigOptions.Generated.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ public static partial class ConfigOptions
159159
defaultValuePlaceholder: "equality_operator|pattern_matching",
160160
description: "Use equality operator or pattern matching as a null check");
161161

162+
public static readonly ConfigOptionDescriptor NullConditionalOperator_AvoidNegativeBooleanComparison = new(
163+
key: ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison,
164+
defaultValue: "false",
165+
defaultValuePlaceholder: "true|false",
166+
description: "Do not suggest to use null-conditional operator when result would be `... != true/false`");
167+
162168
public static readonly ConfigOptionDescriptor NullConditionalOperatorNewLine = new(
163169
key: ConfigOptionKeys.NullConditionalOperatorNewLine,
164170
defaultValue: null,

src/ConfigOptions.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@
237237
<Value>when_type_is_obvious</Value>
238238
</Values>
239239
</Option>
240+
<Option Id="NullConditionalOperator_AvoidNegativeBooleanComparison">
241+
<Key>null_conditional_operator.avoid_negative_boolean_comparison</Key>
242+
<DefaultValue>false</DefaultValue>
243+
<ValuePlaceholder>true|false</ValuePlaceholder>
244+
<Description>Do not suggest to use null-conditional operator when result would be `... != true/false`</Description>
245+
</Option>
240246
<!--
241247
<Option Id="">
242248
<Key></Key>

src/Tests/Analyzers.Tests/RCS1146UseConditionalAccessTests.cs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ void M()
209209
&& f) { }
210210
}
211211
}
212-
""");
212+
""", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
213213
}
214214

215215
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
@@ -235,7 +235,7 @@ void M()
235235
if (x?.Equals(x) == false) { }
236236
}
237237
}
238-
");
238+
", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
239239
}
240240

241241
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
@@ -319,7 +319,7 @@ void M()
319319
if (dic?[0].Equals("x") == false) { }
320320
}
321321
}
322-
""");
322+
""", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
323323
}
324324

325325
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
@@ -363,7 +363,7 @@ Foo M()
363363
Foo M2() => null;
364364
Foo M3() => null;
365365
}
366-
");
366+
", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
367367
}
368368

369369
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
@@ -429,7 +429,7 @@ void M()
429429
if (x?.ToString()?.ToString() != null) { }
430430
}
431431
}
432-
""");
432+
""", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
433433
}
434434

435435
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
@@ -488,7 +488,7 @@ void M()
488488
if (x?.ToString()?.ToString()?.ToString() != null) { }
489489
}
490490
}
491-
");
491+
", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
492492
}
493493

494494
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
@@ -527,7 +527,7 @@ void M()
527527
if (x != null && (x.P != null) is object _) { }
528528
}
529529
}
530-
""");
530+
""", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
531531
}
532532

533533
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
@@ -983,4 +983,42 @@ void M()
983983
}
984984
");
985985
}
986+
987+
[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
988+
public async Task Test_AvoidNegativeBooleanComparison()
989+
{
990+
await VerifyNoDiagnosticAsync(@"
991+
class Foo
992+
{
993+
void M1()
994+
{
995+
Foo x = null;
996+
997+
if (x == null || x.Equals(x)) { }
998+
999+
if (x == default(Foo) || x.Equals(x)) { }
1000+
1001+
if (x == default || x.Equals(x)) { }
1002+
1003+
if (x == null || (x.Equals(x))) { }
1004+
1005+
if (x == null || !x.Equals(x)) { }
1006+
1007+
if (x == null || (!x.Equals(x))) { }
1008+
}
1009+
}
1010+
1011+
struct Foo2
1012+
{
1013+
void M()
1014+
{
1015+
Foo2? x = null;
1016+
1017+
if (x == null || x.Value.Equals(x)) { }
1018+
1019+
if (x == null || !x.Value.Equals(x)) { }
1020+
}
1021+
}
1022+
", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
1023+
}
9861024
}

src/Tools/CodeGeneration/CodeGeneration.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
</PropertyGroup>
66

77
<PropertyGroup>

0 commit comments

Comments
 (0)