Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add option `roslynator_null_conditional_operator.avoid_negative_boolean_comparison` ([PR](https://github.com/dotnet/roslynator/pull/1688))
- Do not suggest to use null-conditional operator when result would be `... != true/false`
- Applicable for [RCS1146](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1146)
- [CLI] Add support for GitLab analyzer reports ([PR](https://github.com/dotnet/roslynator/pull/1633))

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ private static async Task<Document> UseConditionalAccessAsync(
{
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

SyntaxKind kind = binaryExpression.Kind();
SyntaxKind binaryExpressionKind = binaryExpression.Kind();

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

NullCheckStyles allowedStyles = (kind == SyntaxKind.LogicalAndExpression)
NullCheckStyles allowedStyles = (binaryExpressionKind == SyntaxKind.LogicalAndExpression)
? (NullCheckStyles.NotEqualsToNull | NullCheckStyles.IsNotNull)
: (NullCheckStyles.EqualsToNull | NullCheckStyles.IsNull);

Expand Down Expand Up @@ -134,12 +134,12 @@ private static async Task<Document> UseConditionalAccessAsync(
}
case SyntaxKind.LogicalNotExpression:
{
builder.Append((kind == SyntaxKind.LogicalAndExpression) ? " == false" : " != true");
builder.Append((binaryExpressionKind == SyntaxKind.LogicalAndExpression) ? " == false" : " != true");
break;
}
default:
{
builder.Append((kind == SyntaxKind.LogicalAndExpression) ? " == true" : " != false");
builder.Append((binaryExpressionKind == SyntaxKind.LogicalAndExpression) ? " == true" : " != false");
break;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4484,6 +4484,9 @@ string s2 = s as string;]]></Before>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<MinLanguageVersion>6.0</MinLanguageVersion>
<ConfigOptions>
<Option Key="null_conditional_operator.avoid_negative_boolean_comparison" />
</ConfigOptions>
<Samples>
<Sample>
<Before><![CDATA[if (s != null && s.StartsWith("a"))
Expand Down
34 changes: 34 additions & 0 deletions src/Analyzers/CSharp/Analysis/UseConditionalAccessAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ private static void AnalyzeBinaryExpression(SyntaxNodeAnalysisContext context)
if (left is null)
return;

if (context.GetConfigOptions().AvoidNegativeBooleanComparison()
&& WillBeConvertedToNegativeBooleanComparison(binaryExpression, right))
{
return;
}

ISymbol operatorSymbol = context.SemanticModel.GetSymbol(binaryExpression, context.CancellationToken);

if (operatorSymbol?.Name == WellKnownMemberNames.BitwiseOrOperatorName)
Expand Down Expand Up @@ -456,4 +462,32 @@ bool IsFirstChild(SyntaxNode node)
return true;
}
}

private static bool WillBeConvertedToNegativeBooleanComparison(ExpressionSyntax binaryExpression, ExpressionSyntax rightExpression)
{
switch (rightExpression.Kind())
{
case SyntaxKind.LogicalOrExpression:
case SyntaxKind.LogicalAndExpression:
case SyntaxKind.BitwiseOrExpression:
case SyntaxKind.BitwiseAndExpression:
case SyntaxKind.ExclusiveOrExpression:
case SyntaxKind.EqualsExpression:
case SyntaxKind.NotEqualsExpression:
case SyntaxKind.LessThanExpression:
case SyntaxKind.LessThanOrEqualExpression:
case SyntaxKind.GreaterThanExpression:
case SyntaxKind.GreaterThanOrEqualExpression:
case SyntaxKind.IsExpression:
case SyntaxKind.AsExpression:
case SyntaxKind.IsPatternExpression:
{
return false;
}
default:
{
return binaryExpression.IsKind(SyntaxKind.LogicalOrExpression);
}
}
}
}
5 changes: 5 additions & 0 deletions src/Common/CSharp/Extensions/CodeStyleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -621,4 +621,9 @@ private static bool TryGetNewLinePosition(
newLinePosition = NewLinePosition.None;
return false;
}

public static bool AvoidNegativeBooleanComparison(this AnalyzerConfigOptions configOptions)
{
return ConfigOptions.TryGetValueAsBool(configOptions, ConfigOptions.NullConditionalOperator_AvoidNegativeBooleanComparison, out bool value) && value;
}
}
79 changes: 40 additions & 39 deletions src/Common/ConfigOptionKeys.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,45 @@ namespace Roslynator
{
internal static partial class ConfigOptionKeys
{
public const string AccessibilityModifiers = "roslynator_accessibility_modifiers";
public const string AccessorBracesStyle = "roslynator_accessor_braces_style";
public const string ArrayCreationTypeStyle = "roslynator_array_creation_type_style";
public const string ArrowTokenNewLine = "roslynator_arrow_token_new_line";
public const string BinaryOperatorNewLine = "roslynator_binary_operator_new_line";
public const string BlankLineAfterFileScopedNamespaceDeclaration = "roslynator_blank_line_after_file_scoped_namespace_declaration";
public const string BlankLineBetweenClosingBraceAndSwitchSection = "roslynator_blank_line_between_closing_brace_and_switch_section";
public const string BlankLineBetweenSingleLineAccessors = "roslynator_blank_line_between_single_line_accessors";
public const string BlankLineBetweenSwitchSections = "roslynator_blank_line_between_switch_sections";
public const string BlankLineBetweenUsingDirectives = "roslynator_blank_line_between_using_directives";
public const string BlockBracesStyle = "roslynator_block_braces_style";
public const string BodyStyle = "roslynator_body_style";
public const string ConditionalOperatorConditionParenthesesStyle = "roslynator_conditional_operator_condition_parentheses_style";
public const string ConditionalOperatorNewLine = "roslynator_conditional_operator_new_line";
public const string ConfigureAwait = "roslynator_configure_await";
public const string DocCommentSummaryStyle = "roslynator_doc_comment_summary_style";
public const string EmptyStringStyle = "roslynator_empty_string_style";
public const string EnumFlagValueStyle = "roslynator_enum_flag_value_style";
public const string EnumHasFlagStyle = "roslynator_enum_has_flag_style";
public const string EqualsTokenNewLine = "roslynator_equals_token_new_line";
public const string InfiniteLoopStyle = "roslynator_infinite_loop_style";
public const string MaxLineLength = "roslynator_max_line_length";
public const string NewLineAtEndOfFile = "roslynator_new_line_at_end_of_file";
public const string NewLineBeforeWhileInDoStatement = "roslynator_new_line_before_while_in_do_statement";
public const string NullCheckStyle = "roslynator_null_check_style";
public const string NullConditionalOperatorNewLine = "roslynator_null_conditional_operator_new_line";
public const string ObjectCreationParenthesesStyle = "roslynator_object_creation_parentheses_style";
public const string ObjectCreationTypeStyle = "roslynator_object_creation_type_style";
public const string PrefixFieldIdentifierWithUnderscore = "roslynator_prefix_field_identifier_with_underscore";
public const string SuppressUnityScriptMethods = "roslynator_suppress_unity_script_methods";
public const string TabLength = "roslynator_tab_length";
public const string TrailingCommaStyle = "roslynator_trailing_comma_style";
public const string UnityCodeAnalysisEnabled = "roslynator_unity_code_analysis.enabled";
public const string UseAnonymousFunctionOrMethodGroup = "roslynator_use_anonymous_function_or_method_group";
public const string UseBlockBodyWhenDeclarationSpansOverMultipleLines = "roslynator_use_block_body_when_declaration_spans_over_multiple_lines";
public const string UseBlockBodyWhenExpressionSpansOverMultipleLines = "roslynator_use_block_body_when_expression_spans_over_multiple_lines";
public const string UseCollectionExpression = "roslynator_use_collection_expression";
public const string UseVar = "roslynator_use_var";
public const string UseVarInsteadOfImplicitObjectCreation = "roslynator_use_var_instead_of_implicit_object_creation";
public const string AccessibilityModifiers = "roslynator_accessibility_modifiers";
public const string AccessorBracesStyle = "roslynator_accessor_braces_style";
public const string ArrayCreationTypeStyle = "roslynator_array_creation_type_style";
public const string ArrowTokenNewLine = "roslynator_arrow_token_new_line";
public const string BinaryOperatorNewLine = "roslynator_binary_operator_new_line";
public const string BlankLineAfterFileScopedNamespaceDeclaration = "roslynator_blank_line_after_file_scoped_namespace_declaration";
public const string BlankLineBetweenClosingBraceAndSwitchSection = "roslynator_blank_line_between_closing_brace_and_switch_section";
public const string BlankLineBetweenSingleLineAccessors = "roslynator_blank_line_between_single_line_accessors";
public const string BlankLineBetweenSwitchSections = "roslynator_blank_line_between_switch_sections";
public const string BlankLineBetweenUsingDirectives = "roslynator_blank_line_between_using_directives";
public const string BlockBracesStyle = "roslynator_block_braces_style";
public const string BodyStyle = "roslynator_body_style";
public const string ConditionalOperatorConditionParenthesesStyle = "roslynator_conditional_operator_condition_parentheses_style";
public const string ConditionalOperatorNewLine = "roslynator_conditional_operator_new_line";
public const string ConfigureAwait = "roslynator_configure_await";
public const string DocCommentSummaryStyle = "roslynator_doc_comment_summary_style";
public const string EmptyStringStyle = "roslynator_empty_string_style";
public const string EnumFlagValueStyle = "roslynator_enum_flag_value_style";
public const string EnumHasFlagStyle = "roslynator_enum_has_flag_style";
public const string EqualsTokenNewLine = "roslynator_equals_token_new_line";
public const string InfiniteLoopStyle = "roslynator_infinite_loop_style";
public const string MaxLineLength = "roslynator_max_line_length";
public const string NewLineAtEndOfFile = "roslynator_new_line_at_end_of_file";
public const string NewLineBeforeWhileInDoStatement = "roslynator_new_line_before_while_in_do_statement";
public const string NullCheckStyle = "roslynator_null_check_style";
public const string NullConditionalOperator_AvoidNegativeBooleanComparison = "roslynator_null_conditional_operator.avoid_negative_boolean_comparison";
public const string NullConditionalOperatorNewLine = "roslynator_null_conditional_operator_new_line";
public const string ObjectCreationParenthesesStyle = "roslynator_object_creation_parentheses_style";
public const string ObjectCreationTypeStyle = "roslynator_object_creation_type_style";
public const string PrefixFieldIdentifierWithUnderscore = "roslynator_prefix_field_identifier_with_underscore";
public const string SuppressUnityScriptMethods = "roslynator_suppress_unity_script_methods";
public const string TabLength = "roslynator_tab_length";
public const string TrailingCommaStyle = "roslynator_trailing_comma_style";
public const string UnityCodeAnalysisEnabled = "roslynator_unity_code_analysis.enabled";
public const string UseAnonymousFunctionOrMethodGroup = "roslynator_use_anonymous_function_or_method_group";
public const string UseBlockBodyWhenDeclarationSpansOverMultipleLines = "roslynator_use_block_body_when_declaration_spans_over_multiple_lines";
public const string UseBlockBodyWhenExpressionSpansOverMultipleLines = "roslynator_use_block_body_when_expression_spans_over_multiple_lines";
public const string UseCollectionExpression = "roslynator_use_collection_expression";
public const string UseVar = "roslynator_use_var";
public const string UseVarInsteadOfImplicitObjectCreation = "roslynator_use_var_instead_of_implicit_object_creation";
}
}
6 changes: 6 additions & 0 deletions src/Common/ConfigOptions.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ public static partial class ConfigOptions
defaultValuePlaceholder: "equality_operator|pattern_matching",
description: "Use equality operator or pattern matching as a null check");

public static readonly ConfigOptionDescriptor NullConditionalOperator_AvoidNegativeBooleanComparison = new(
key: ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison,
defaultValue: "false",
defaultValuePlaceholder: "true|false",
description: "Do not suggest to use null-conditional operator when result would be `... != true/false`");

public static readonly ConfigOptionDescriptor NullConditionalOperatorNewLine = new(
key: ConfigOptionKeys.NullConditionalOperatorNewLine,
defaultValue: null,
Expand Down
6 changes: 6 additions & 0 deletions src/ConfigOptions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@
<Value>when_type_is_obvious</Value>
</Values>
</Option>
<Option Id="NullConditionalOperator_AvoidNegativeBooleanComparison">
<Key>null_conditional_operator.avoid_negative_boolean_comparison</Key>
<DefaultValue>false</DefaultValue>
<ValuePlaceholder>true|false</ValuePlaceholder>
<Description>Do not suggest to use null-conditional operator when result would be `... != true/false`</Description>
</Option>
<!--
<Option Id="">
<Key></Key>
Expand Down
52 changes: 45 additions & 7 deletions src/Tests/Analyzers.Tests/RCS1146UseConditionalAccessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ void M()
&& f) { }
}
}
""");
""", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
}

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

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
Expand Down Expand Up @@ -319,7 +319,7 @@ void M()
if (dic?[0].Equals("x") == false) { }
}
}
""");
""", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
Expand Down Expand Up @@ -363,7 +363,7 @@ Foo M()
Foo M2() => null;
Foo M3() => null;
}
");
", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
Expand Down Expand Up @@ -429,7 +429,7 @@ void M()
if (x?.ToString()?.ToString() != null) { }
}
}
""");
""", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
Expand Down Expand Up @@ -488,7 +488,7 @@ void M()
if (x?.ToString()?.ToString()?.ToString() != null) { }
}
}
");
", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
}

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

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
Expand Down Expand Up @@ -983,4 +983,42 @@ void M()
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseConditionalAccess)]
public async Task Test_AvoidNegativeBooleanComparison()
{
await VerifyNoDiagnosticAsync(@"
class Foo
{
void M1()
{
Foo x = null;

if (x == null || x.Equals(x)) { }

if (x == default(Foo) || x.Equals(x)) { }

if (x == default || x.Equals(x)) { }

if (x == null || (x.Equals(x))) { }

if (x == null || !x.Equals(x)) { }

if (x == null || (!x.Equals(x))) { }
}
}

struct Foo2
{
void M()
{
Foo2? x = null;

if (x == null || x.Value.Equals(x)) { }

if (x == null || !x.Value.Equals(x)) { }
}
}
", options: Options.AddConfigOption(ConfigOptionKeys.NullConditionalOperator_AvoidNegativeBooleanComparison, true));
}
}
2 changes: 1 addition & 1 deletion src/Tools/CodeGeneration/CodeGeneration.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public static string GenerateEditorConfig(RoslynatorMetadata metadata, bool comm
w.WriteAnalyzer(
analyzer.Id.ToLowerInvariant(),
(analyzer.IsEnabledByDefault)
? ((DiagnosticSeverity)Enum.Parse(typeof(DiagnosticSeverity), analyzer.DefaultSeverity)).ToReportDiagnostic()
? Enum.Parse<DiagnosticSeverity>(analyzer.DefaultSeverity).ToReportDiagnostic()
: ReportDiagnostic.Suppress);

if (analyzer.ConfigOptions.Count > 0)
Expand Down
Loading
Loading